Skip to content

Web Support #122

Open
Open

Description

@connectety

Please add support for Flutter web.
If saving infos needs be done, maybe the localStorage in webbrowsers can help.

Activity

  1. renefloor commented on Nov 1, 2019

    @renefloor
    Contributor

    The problem is not storing the files, but that it uses sqflite to store the cache info and that doesn't support Flutter Web yet: tekartik/sqflite#212

    I'll look into this whether a switch to Moor or Hive https://github.com/renefloor/flutter_cache_manager/issues/121 is a good move. We'll have to migrate all the data, so I really have to study all the pros and cons before making a decision.

  2. connectety commented on Nov 2, 2019

    @connectety
    Author

    I looked into it a little bit more.
    if I understand everything correctly and did not forget any type of storage,
    localStorage has a maximum of 10 MB per origin and
    cookies have a maximum of 4 KB per domain and
    indexDB can get quite big but is not persistent.

    In my opinion this is not enough space to cache files.
    But webbrowser cache files themselves quite good.

    As a workaround I would propose, not caching files for web, but giving a warning and / or exception, which has to be dismissed.

  3. renefloor commented on Nov 4, 2019

    @renefloor
    Contributor

    @connectety I'm not completely sure what you propose. Do you mean that if the platform is Web the library just uses NetworkImage instead of CachedImage? Devs will expect it to work differently.

  4. connectety commented on Nov 5, 2019

    @connectety
    Author

    use NetworkImage instead of CachedImage

    Yes, for example.

    Devs will expect it to work differently.

    I know, and this is not the best idea I have ever had.
    I do not stand behind it completely, but I don't see any way to save content and a database, because the storage provided by web browsers is just too small.

    Hopefully, I have forgotten some kind of storage.

    And also I want to use this as a Hotfix, which can only be used after dismissing an exception or setting a flag or something. So that people will understand that it does not work as expected.
    This idea should still somewhat work because there is already pretty good caching in the web and browsers.

    Even if this objectively bad solution gets dismissed, I wanted to point out the Problem.

  5. alextekartik commented on Dec 23, 2019

    @alextekartik

    I started a fork (https://github.com/alextekartik/flutter_cache_manager and https://github.com/alextekartik/flutter_cached_network_image) where I experimented caching for the web by abstracting the file system (using fs_shim) and the storage (using idb_shim). It is definitely not perfect and a better solution could be found but I think having a way to abstract the database and file system into flutter_cache_manager could allow anyone to use their preferred storage (hive, moor, sqlite, indexed db....)

    But with CORS and web browser already doing some caching, the web is a different experience that could require a specific and different network caching mechanism

  6. IchordeDionysos commented on Feb 10, 2020

    @IchordeDionysos

    Why not use service workers for caching? 🤔

    This is what the web does really good out of the box. (In newer browser versions :D)
    Also with the Cache API, you can save the most files with at least 50MB storage.

    This might need some refactoring of this package to use sqflite for mobile and service workers for web (maybe using the plugin_platform_interface)

    Useful links:
    https://developer.mozilla.org/en-US/docs/Web/API/Cache

  7. deakjahn commented on May 16, 2020

    @deakjahn

    Until the support arrives, could it be made to fail silently? :-) Right now

    await DefaultCacheManager().emptyCache();
    

    never returns for me in a web app. Yes, it's easy to wrap it into an if (!kIsWeb), just saying...

  8. renefloor commented on Jun 20, 2020

    @renefloor
    Contributor

    It doesn't cache on disk at the moment, but as @deakjahn asked it at least doesn't fail. It nicely returns the file which is just downloaded from the internet.

    @IchordeDionysos I'm looking at the caching options for web. Thanks for the url

  9. deakjahn commented on Jun 20, 2020

    @deakjahn

    Actually, I store my files with the Cache API (not necessarily these ones but generally). Do you need some code so that you don't need to look it up yourself? Fairly straightforward.

  10. renefloor commented on Jun 20, 2020

    @renefloor
    Contributor

    Yes it is always good to see some code, but it looks really straight forward indeed.

    The main challenge I think at the moment is adapting the cache manager to separate the current storage from the web api.

  11. deakjahn commented on Jun 20, 2020

    @deakjahn

    I have an app-embedded platform plugin that simply offers a file storage interface for my app and gets translated to either plain old dart.io or Cache API, depending on the platform. It's tailored to my specific needs, so it's not perfect for immediate re-use without some tweaking. Basically, whenever the normal file IO speaks about folders, I use separate cache files on the web and treat a path like "foo/bar" as an item stored with the name "bar" inside a cache file named "foo". Nothing more nested than this, this was enough for my purposes.

    So, the interface itself is this:

    Future<bool> fileSave(String path, Uint8List contents, {String mime, bool flush = false});
    Future<bool> fileSaveString(String path, String contents, {String mime, bool flush = false});
    Future<Uint8List> fileRead(String path, {String mime});
    Future<String> fileReadString(String path, {String mime});
    Future<bool> fileExists(String path);
    Future<int> fileSize(String path);
    Future<bool> fileDelete(String path);
    Future<bool> fileCopy(String src, String dest);
    Future<bool> folderCreate(String path, {bool recursive = false});
    Future<bool> folderDelete(String path, {bool recursive = false});
    Future<bool> folderEmpty(String path);
    Future<bool> folderExists(String path);
    Future<List<String>> folderList(String path);

    Not that you would need all of them, especially not the ones that store a string (basically, a text file).

  12. deakjahn commented on Jun 20, 2020

    @deakjahn

    The implementation is relatively simple:

    import 'dart:html' as html;
    import 'dart:js';
    import 'dart:js_util' as util;
    
    const String _BINARY = 'application/octet-stream';
    const String _TEXT = 'text/plain';
    
    Future<bool> fileSave<T>(String path, T contents, String mime) async {
      final parts = path.splitFirst('/');
      return util.promiseToFuture<bool>(_nativeCacheSave(parts.first, parts.last, contents, mime));
    }
    
    Future<T> fileRead<T>(String path, String mime) async {
      final parts = path.splitFirst('/');
      if (mime == _BINARY) {
        final buffer = await util.promiseToFuture<ByteBuffer>(await _nativeCacheRead(parts.first, parts.last));
        return buffer.asUint8List() as T;
      } else if (mime == _TEXT)
        return await util.promiseToFuture<String>(_nativeCacheReadString(parts.first, parts.last)) as T;
      else
        throw UnsupportedError(mime);
    }
    
    Future<bool> fileExists(String path) async {
      final parts = path.splitFirst('/');
      return util.promiseToFuture<bool>(_nativeCacheExists(parts.first, parts.last));
    }
    
    Future<int> fileSize(String path) {
      final parts = path.splitFirst('/');
      return util.promiseToFuture<int>(_nativeCacheSize(parts.first, parts.last));
    }
    
    Future<bool> fileDelete(String path) async {
      final parts = path.splitFirst('/');
      return util.promiseToFuture<bool>(_nativeCacheDelete(parts.first, parts.last));
    }
    
    Future<bool> fileCopy(String src, String dest) async {
      final parts1 = src.splitFirst('/');
      final parts2 = dest.splitFirst('/');
      return util.promiseToFuture<bool>(_nativeCacheCopy(parts1.first, parts1.last, parts2.first, parts2.last));
    }
    
    Future<bool> folderCreate(String path) async {
      final parts = path.splitFirst('/');
      await html.window.caches.open(parts.first);
      return true;
    }
    
    Future<bool> folderDelete(String path) async {
      final parts = path.splitFirst('/');
      return await html.window.caches.delete(parts.first);
    }
    
    Future<bool> folderEmpty(String path) async {
      await folderDelete(path);
      await folderCreate(path);
      return true;
    }
    
    Future<bool> folderExists(String path) async {
      final parts = path.splitFirst('/');
      return await html.window.caches.has(parts.first);
    }
    
    Future<List<String>> folderList(String path) async {
      final files = await util.promiseToFuture<List<dynamic>>(_nativeCacheList(path));
      return files.map((f) => f.toString()).toList();
    }

    with a simple helper:

    extension Splits on String {
      List<String> splitFirst(Pattern pattern) {
        final parts = split(pattern);
        return [
          if (parts.isNotEmpty) parts.first,
          if (parts.length > 1) parts.sublist(1).join(pattern),
        ];
      }
    }
  13. deakjahn commented on Jun 20, 2020

    @deakjahn

    The native (JavaScript, that is) implementations are:

    @JS('cacheSave')
    external bool _nativeCacheSave(String name, String path, dynamic contents, String mime);
    
    @JS('cacheRead')
    external JsObject _nativeCacheRead(String name, String path);
    
    @JS('cacheReadString')
    external String _nativeCacheReadString(String name, String path);
    
    @JS('cacheCopy')
    external bool _nativeCacheCopy(String name1, String src, String name2, String dest);
    
    @JS('cacheExists')
    external bool _nativeCacheExists(String name, String path);
    
    @JS('cacheSize')
    external int _nativeCacheSize(String name, String path);
    
    @JS('cacheDelete')
    external bool _nativeCacheDelete(String name, String path);
    
    @JS('cacheList')
    external JsArray _nativeCacheList(String name);
    cacheSave: async function(name, path, contents, mime) {
      var cache = await window.caches.open(name);
      var blob = new Blob([contents], {type : mime});
      var options = {headers: {'Content-Type': mime, 'Content-Length': contents.length}};
      await cache.put(path, new Response(blob, options));
      return true;
    },
    
    cacheRead: async function(name, path) {
      var cache = await window.caches.open(name);
      var options = {ignoreSearch: true, ignoreMethod: true, ignoreVary: true};
      var data = await cache.match(path, options);
      return data.arrayBuffer();
    },
    
    cacheReadString: async function(name, path) {
      var cache = await window.caches.open(name);
      var options = {ignoreSearch: true, ignoreMethod: true, ignoreVary: true};
      var data = await cache.match(path, options);
      return data.text();
    },
    
    cacheCopy: async function(name1, src, name2, dest) {
      var cache1 = await window.caches.open(name1);
      var options = {ignoreSearch: true, ignoreMethod: true, ignoreVary: true};
      var data = await cache1.match(src, options);
      var cache2 = await window.caches.open(name2);
      await cache2.put(dest, data);
      return true;
    },
    
    cacheSize: async function(name, path) {
      var cache = await window.caches.open(name);
      var options = {ignoreSearch: true, ignoreMethod: true, ignoreVary: true};
      var data = await cache.match(path, options);
      return parseInt(data.headers.get('Content-Length'));
    },
    
    cacheExists: async function(name, path) {
      var cache = await window.caches.open(name);
      return await cache.match(path) != undefined;
    },
    
    cacheDelete: async function(name, path) {
      var cache = await window.caches.open(name);
      return await cache.delete(path);
    },
    
    cacheList: async function(name) {
      var cache = await window.caches.open(name);
      var files = await cache.keys();
      return files.map(f => new URL(f.url).pathname);
    },

    But this supposes that you already have a JS file that you can put them into. If you don't, I can also show you how I normally attach one (very much like some of my plugins, eg. https://github.com/deakjahn/flutter_dropzone, but you don't need the added complexity of a platform view, of course). Basically, the few lines starting from here, called from whatever initialization function you have handy.

  14. deakjahn commented on Jun 20, 2020

    @deakjahn

    Oh yes, and of course, you need to make sure you have the support:

    cacheAvailable = window.caches != undefined;

    You do with any sane browser you might encounter today but still... :-)

  15. renefloor commented on Jun 20, 2020

    @renefloor
    Contributor

    But this supposes that you already have a JS file that you can put them into.

    If you use dart:html in a Flutter app, isn't this automatically added?

      var blob = new Blob([contents], {type : mime});
      var options = {headers: {'Content-Type': mime, 'Content-Length': contents.length}};
      await cache.put(path, new Response(blob, options));
    

    You basically take the content from the response and put them in a new response. Why wouldn't you just directly add the response from the server?

    you need to make sure you have the support

    Yes of course, thanks for that as well. Especially for older iOS devices (iOS 10 and lower) this is important.

  16. 43 remaining items

  17. deakjahn commented on Jul 16, 2020

    @deakjahn

    One problem at a time. :-))

    What does this version do for you?

    Future<T> _toFuture<T>(dynamic promise) {
      final completer = Completer<T>();
      (promise as js.JsObject).callMethod('then', [
        (result) => completer.complete(result)
      ]);
      return completer.future;
    }
    

    It still works over here.

  18. renefloor commented on Jul 16, 2020

    @renefloor
    Contributor

    image

    I use Intellij, which is the same as Android Studio, but that shouldn't matter for the analysis.

  19. deakjahn commented on Jul 16, 2020

    @deakjahn

    Yes, it shouldn't. Now I started to see something. I didn't open the whole package with AS or whatever IDE earlier because I didn't see the need. I just modified those three files in the flutter pub cache directory and compiled my test app (the test app was under the IDE, of course, just not your package).

    If I open your package in the IDE, I get the same errors. However, if I just compile an app that uses your package, there are no errors at all, it compiles and runs. Go figure.

  20. deakjahn commented on Jul 16, 2020

    @deakjahn

    I start to get the feeling that this is pedantic stuff. :-)

  21. deakjahn commented on Jul 16, 2020

    @deakjahn

    However, still with IOSink because that should be solved first. You can simply remove it from the IO but I don't know how you want to handle it on the Web.

  22. wer-mathurin commented on Dec 1, 2020

    @wer-mathurin

    Any status on this?

  23. renefloor commented on Dec 1, 2020

    @renefloor
    Contributor

    Not at the moment.

  24. mrtdeh commented on Jul 16, 2021

    @mrtdeh

    any update on this?

  25. davemg3 commented on Nov 21, 2021

    @davemg3

    Hello,
    Is this still scheduled to add web support on flutter cache manager?
    thks

  26. tomekit commented on Jan 29, 2023

    @tomekit

    I was wondering if there are still plans to add a Web support. It would greatly improve: flutter_cached_network_image caching capabilities.

  27. duck-dev-go commented on Nov 28, 2024

    @duck-dev-go

    Any update on this?

  28. kszczek commented on May 31, 2025

    @kszczek

    This feature can now be easily implemented using pure Dart with no supporting JS code thanks to the recently released package:web. Here's a quick demo:

    import 'dart:js_interop';
    
    import 'package:flutter/material.dart';
    import 'package:web/web.dart' show Response, window;
    
    void main() {
      runApp(const CacheApiDemo());
    }
    
    class CacheApiDemo extends StatelessWidget {
      const CacheApiDemo({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Cache API Demo',
          home: Scaffold(
            appBar: AppBar(title: Text('Cache API Demo'), centerTitle: true),
            body: Center(
              child: SizedBox(
                width: 300,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  spacing: 8.0,
                  children: <Widget>[
                    FilledButton(onPressed: _read, child: Text('Read')),
                    FilledButton(onPressed: _write, child: Text('Write')),
                    FilledButton(onPressed: _delete, child: Text('Delete')),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    
      Future<void> _read() async {
        final cache = await window.caches.open('testCache').toDart;
        final result = await cache.match('testKey'.toJS).toDart;
    
        if (result != null) {
          print(await result.text().toDart);
        } else {
          print('testKey missing!');
        }
      }
    
      Future<void> _write() async {
        final cache = await window.caches.open('testCache').toDart;
        await cache.put('testKey'.toJS, Response('testValue'.toJS)).toDart;
      }
    
      Future<void> _delete() async {
        final cache = await window.caches.open('testCache').toDart;
        await cache.delete('testKey'.toJS).toDart;
      }
    }

    The Cache API allows us to store an enourmous amount of data (up to 60% of total disk space in Chrome and Safari!).

    There is a small issue to think about though:

    cache.put('testKey'.toJS, Response('testValue'.toJS)).toDart;
    cache.put('testKey'.toJS, Response('anotherValue'.toJS)).toDart;

    I haven't read the spec so I'm not sure if the .put() method guarantees that the call order will reflect the order in which the values will be persisted, but in case it does not guarantee that, then I suggest we use the synchronous local storage API as a mutex, this way we'll also be protected from simultaneous cross-tab writes.

  29. nicolarigoni commented on Mar 30, 2026

    @nicolarigoni

    @ben-milanko hi, any update on this? Thanks

  30. ben-milanko commented on Mar 30, 2026

    @ben-milanko

    Hey @nicolarigoni ,

    I've made a PR for this change #504

    But looking at the repo the last update was 2024, so I'm not sure if this is still actively being maintained.

    If you would like to use it, you can include the below in your pubspec yaml.

      cached_network_image:
        git:
          url: https://github.com/Railway-Engineering-Solutions/flutter_cached_network_image.git
          ref: develop
      flutter_cache_manager: #^3.1.2
        git:
          url: https://github.com/Railway-Engineering-Solutions/flutter_cache_manager.git
          ref: develop
          path: flutter_cache_manager
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions