Newby Coder header banner

Flutter Cache Storage

Implementing Cache storage in Flutter Android/iOS application

Cache storage make use of Application Files directory in Android and Documents directory in iOS to store and retrieve files and enable storage of complex data types when compared to shared preferences and json storage

flutter_cache_manager package provides BaseCacheManager which can be inherited to access functions to store and remove files from cache

It also supports file download from a url, which is implicitly stored in Cache


Creating new Flutter App

Check Flutter installation to setup Flutter

Use flutter create command to create a Flutter project (here cache_storage_app) :

flutter create cache_storage_app

Dependency


Implementation

Importing Package
import 'package:flutter_cache_manager/flutter_cache_manager.dart';

Declare a cache manager

Create a class which inherits from BaseCacheManager

Implement getFilePath() method which returns path of where files are to be stored

getApplicationDocumentsDirectory() (which requires path_provider package) returns path of documents directory of a specific application (sandboxed in iOS)

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';

class ImageCacheManager extends BaseCacheManager {
  static const key = "imageCache";
  static ImageCacheManager _instance;

  factory ImageCacheManager() {
    if (_instance == null) {
      _instance = new ImageCacheManager._();
    }
    return _instance;
  }

  ImageCacheManager._() : super(key);

  Future<String> getFilePath() async {
    var directory = await getApplicationDocumentsDirectory();
    return p.join(directory.path, key);
  }
}

Store file

Use putFile to store files in Uint8List format

await ImageCacheManager()
  .putFile(fileName, Uint8List.fromList(json.encode(someObject).codeUnits));

A file can be converted to Uint8List using readAsBytesSync()

Uint8List intList = file.readAsBytesSync()

Get file

Map<String, dynamic> map = new Map();
    await ImageCacheManager().getFileFromCache(fileName)
    .then((fileInfo) async {
      if(fileInfo?.file != null ) {
        String fileStr = await fileInfo.file.readAsString();
      }
    })
    .catchError((e) {
      print("UrlImagesList init error ${e.toString()}");
    });

Get file from cache and/or online

ImageCacheManager().getSingleFile(image_url).listen((fileInfo) {
      image_url = fileInfo?.file?.path;
    })
    .asFuture(() {}).then((e) {})
    .timeout(Duration(seconds:5), onTimeout: () {})
    .catchError((e) {});

Remove file

await ImageCacheManager().removeFile(image_url);

Empty cache

ImageCacheManager().emptyCache();

App Code

Following example app takes a url input from user, uses http.get() to get webpage content and parses the webpage to get links of images present in the webpage

ImageCacheManager is declared which extends from BaseCacheManager and is used to download and cache the images, so that they persist on app restarts

The home screen widget renders a TabView whose selected index is stored to persist over restarts (check app with state persistence)

Cache storage is implemented in CacheStorageWidget

import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:state_persistence/state_persistence.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';

import 'package:flutter/services.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';


void main() => runApp(PersistentStorageApp());

class PersistentStorageApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PersistedAppState(
      storage: JsonFileStorage(initialData: {
        'tab': 1,
      }),
      child: MaterialApp(
        title: 'Persistent Storage Example App',
        theme: ThemeData(primarySwatch: Colors.blue),
        home: EgWidget(),
      ),
    );
  }
}

class EgWidget extends StatefulWidget {
  @override
  _EgWidgetState createState() => _EgWidgetState();
}

class _EgWidgetState extends State<EgWidget> with SingleTickerProviderStateMixin {
  PersistedData _data;
  TabController _controller;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _data = PersistedAppState.of(context);
    if (_data != null && _controller == null) {
      _controller = TabController(initialIndex: _data['tab'] ?? 0, vsync: this, length: 4);
      _controller.addListener(_onTabChanged);
    }
  }

  void _onTabChanged() {
    if (!_controller.indexIsChanging) {
      _data['tab'] = _controller.index;
    }
  }

  @override
  void dispose() {
    _controller?.removeListener(_onTabChanged);
    _controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_data != null) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Persistent Tab Example'),
          bottom: TabBar(
            controller: _controller,
            tabs: [
              Tab(text: 'Tab 1'),
              Tab(text: 'Shared Preference'),
              Tab(text: 'Json Storage'),
              Tab(text: 'Cache Storage'),
            ],
          ),
        ),
        body: TabBarView(
          controller: _controller,
          children: [
            Container(color: Colors.cyanAccent[400], child: Center(child: Text('Tab 1', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)))),
            Container(color: Colors.amber[600], child: Center(child: Text('Tab 2', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)))),
            Container(color: Colors.greenAccent[700], child: Center(child: Text('Tab 3', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)))),
            CacheStorageWidget(),
          ],
        )
      );
    }
    else {
      return Center(child: CircularProgressIndicator());
    }
  }
}

class UrlImages {
  int id;
  String url;
  Map<String, dynamic> imagePathMap;

  static UrlImages from(jsonObj) {
    UrlImages urlImages = new UrlImages();
    urlImages.id = jsonObj["counter"];
    urlImages.url = jsonObj["url"];
    urlImages.imagePathMap = jsonObj["imagePathMap"];
    return urlImages;
  }

  Map<String, dynamic> toJsonEncodable() {
    Map<String, dynamic> jsonObj = new Map();
    jsonObj["id"] = id;
    jsonObj["url"] = url;
    jsonObj["imagePathMap"] = imagePathMap;
    return jsonObj;
  }

  static Future<UrlImages> forUrl(String url, int counter) async {
    UrlImages urlImages = new UrlImages();
    urlImages.id = counter;
    urlImages.url = url;
    await urlImages._getImagePathMap();
    await urlImages.downloadFiles();
    return urlImages;
  }

  Future<Map<String, dynamic>> _getImagePathMap() async {
    http.Client _client = http.Client();
    http.Response _response;
    var _elements;
    dom.Document _document;
    imagePathMap = new Map();
    try {
      _response = await _client.get('$url').timeout(Duration(seconds:10), onTimeout: () {});
      _document = parse(_response.body);
      _elements = _document.querySelectorAll('img');
    }
    catch (error) {
      print('_getImagePathMap error: $error');
    }
    if(_elements != null)
      _elements.forEach((element) {
      try {
          var src = getSrcFromElement(element);
          if(src != null)
            imagePathMap[src] = null;
        }
        catch(e) {
          print("_elements error ${e.toString()}");
        }
      }
    );
    return imagePathMap;
  }

  String getSrcFromElement(element) {
    final srcAttrRegex = RegExp(r'src=\s*"([^"]*)"');
    var src = srcAttrRegex.firstMatch(element.outerHtml)?.group(1);
    if (src != null) {
      if(!src.startsWith("http")) {
        if(src.startsWith("//")) {
          var protocol = RegExp(r'(http[s]*:)').firstMatch(url)?.group(1);
          src = "${protocol}${src}";
        }
        else {
          var domain = RegExp(r'(http[s]*:[/]*[^/]*)[/]*').firstMatch(url)?.group(1);
          if(domain != null) {
            src = src.startsWith("/")? src.substring(1) : src;
            src = "${domain}/${src}";
          }
        }
      }
    }
    return src;
  }

  void downloadFiles() async {
    if(imagePathMap != null) {
      for(String image_url in imagePathMap.keys)
        await _downloadFile(image_url);
    }
  }

  void _downloadFile(image_url) async {
    await ImageCacheManager().getSingleFile(image_url).listen((fileInfo) {
      imagePathMap[image_url] = fileInfo?.file?.path;
    })
    .asFuture(() {}).then((e) {})
    .timeout(Duration(seconds:5), onTimeout: () {})
    .catchError((e) {
      imagePathMap[image_url] = "Error ${e.toString()}";
      print("download file ${image_url} error ${e.toString()}");
    });
  }
}

class UrlImagesList {
  static const fileName = "local-json.json";
  int counter;
  List<UrlImages> urlImageList;

  static UrlImagesList _instance;

  factory UrlImagesList() {
    if (_instance == null) {
      _instance = new UrlImagesList._();
      _instance.counter = 0;
      _instance.urlImageList = new List();
    }
    return _instance;
  }

  UrlImagesList._();

  void init() async {
    Map<String, dynamic> map = new Map();
    await ImageCacheManager().getFileFromCache(fileName)
    .then((fileInfo) async {
      if(fileInfo?.file != null ) {
        String fileStr = await fileInfo.file.readAsString();
        map = json.decode(fileStr);
        counter = map["counter"];
        for(dynamic each in map["urlImageList"]) {
          urlImageList.add(UrlImages.from(each));
        }
      }
    })
    .catchError((e) {
      print("UrlImagesList init error ${e.toString()}");
    });
  }

  Map<String, dynamic> toJsonEncodable() {
    Map<String, dynamic> map = new Map();
    map["counter"] = counter;
    map["urlImageList"] = new List<Map>();
    for(var each in urlImageList) {
      map["urlImageList"].add(each.toJsonEncodable());
    }
    return map;
  }

  void putFile() async {
    await ImageCacheManager()
      .putFile(fileName, Uint8List.fromList(json.encode(toJsonEncodable()).codeUnits));
  }

  void removeUrl(String url) async {
    urlImageList.removeWhere((urlImages_) {
      return urlImages_.url == url;
    });
    await putFile();
  }

  Future<List<UrlImages>> addUrl(String url) async {
    UrlImages urlImages;
    if(!(url == null || url.length <1 )) {
      url = url.toLowerCase().trim();
      if(!url.startsWith("http"))
        url = 'https://${url}';
    if(!(urlImageList.any((_urlImages) {return _urlImages.url == url;}))) {
        counter ++;
        urlImages = await UrlImages.forUrl(url, counter);
        if(urlImages != null) {
          urlImageList.add(urlImages);
          await putFile();
        }
      }
    }
    return urlImageList;
  }

  Future<void> removeLocalFile(String url, String image_url) async {
    try {
      await ImageCacheManager().removeFile(image_url);
      urlImageList[urlImageList.indexWhere((_urlImages) {
        return _urlImages.url == url;})]
        .imagePathMap.removeWhere((key, value) {
          return key == image_url;
        });
      imageCache.clear();
      await putFile();
    } catch(e) { print("file removed error ${e}"); };
  }
}

class CacheStorageWidget extends StatefulWidget {
  CacheStorageWidget({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _CacheStorageWidgetState createState() => _CacheStorageWidgetState();
}

class _CacheStorageWidgetState extends State<CacheStorageWidget> {
  Widget imagesContainer = Container();
  bool addUrlButtonEnabled = true;
  TextEditingController controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    init();
  }

  void init() async {
    await UrlImagesList().init();
    setState(() {});
  }

  Widget getUrlImagesContainer(UrlImages urlImages) {
    List<Widget> cclist = new List();
    cclist.add(ListTile(title: Text("${urlImages.url}"), trailing : IconButton(
      icon: Icon(Icons.delete),
      onPressed: () async {
        await UrlImagesList().removeUrl(urlImages.url);
        setState(() {});
      },
    )));
    urlImages.imagePathMap?.keys.forEach((key) {
        cclist.add(getImageColumn(urlImages.url, key, urlImages.imagePathMap[key]));
      });
    return Container(
      padding: EdgeInsets.all(10),
      child: Card(
        child: Column(
          children: cclist,
        ),
      ),
    );
  }

  removeLocalFile(url, image_url) async {
    await UrlImagesList().removeLocalFile(url, image_url);
    setState(() {});
  }

  Widget getImageColumn(String url, String image_url, String localPath) {
    List<Widget> ccList = new List();
    ccList.add(ListTile(subtitle: Text("${image_url}"), trailing : IconButton(
        icon: Icon(Icons.cancel),
        tooltip: 'Delete pic',
        onPressed: () {
          removeLocalFile(url, image_url);
        },
      )));
    if(localPath != null) {
      if(localPath.startsWith("Error")) {
        ccList.add(Text("localPath error : ${localPath.split(' ')[1]}"));
      }
      else {
        ccList.add(Text("localPath : ${localPath}"));
        ccList.add(Image.file(File(localPath), fit:BoxFit.contain));
      }
    }
    return Card(
      child: Container (
        padding: EdgeInsets.only(left:5, right:5, top:10, bottom:10),
        child: Column(
          children: ccList,
        ),
      ),
    );
  }

  Future<Widget> getImagesWidget() async {
    List<Widget> cclist = new List();
    if(!addUrlButtonEnabled) {
      await UrlImagesList().addUrl(controller.text);
      addUrlButtonEnabled = true;
    }
    UrlImagesList().urlImageList.forEach((urlImages)
      { cclist.add(getUrlImagesContainer(urlImages)); });
    return Container(
      child: Column(
        children: cclist,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Cache images"),
      ),
      body: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new TextFormField(
              decoration: new InputDecoration(hintText: 'url', labelText: 'Url'),
              controller: controller
            ),
            Padding(
              padding: const EdgeInsets.only(top: 32.0),
              child: RaisedButton(
                child: Text('Add url'),
                onPressed: () {
                  if(addUrlButtonEnabled) {
                    addUrlButtonEnabled = false;
                    setState((){});
                  }
                },
              ),
            ),
            FutureBuilder<Widget>(
              future: getImagesWidget(),
              builder: (context, snapshot) {
                if(snapshot.connectionState == ConnectionState.done) {
                  if (snapshot.hasData) {
                    return snapshot.data;
                  }
                  else if (snapshot.hasError) {
                    return Text("${snapshot.error}");
                  }
                }
                return CircularProgressIndicator();
              },
            ),
            Padding(
              padding: const EdgeInsets.only(top: 32.0),
              child: RaisedButton(
                child: Text('Clear Cache'),
                onPressed: () async {
                  await ImageCacheManager().emptyCache();
                  imageCache.clear();
                  setState((){
                  });
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ImageCacheManager extends BaseCacheManager {
  static const key = "imageCache";
  static ImageCacheManager _instance;

  factory ImageCacheManager() {
    if (_instance == null) {
      _instance = new ImageCacheManager._();
    }
    return _instance;
  }

  ImageCacheManager._() : super(key);

  Future<String> getFilePath() async {
    var directory = await getApplicationDocumentsDirectory();
    return p.join(directory.path, key);
  }
}

Run instructions

Ensure a supported device is connected or emulator/simulator is started

Go to project directory

Use flutter run command to run

flutter run

It builds and runs app on an available android/ios device


Screenshot/image

Android

cl-flutter-local-storage-cache-images

iOS

cm-flutter-local-storage-cache-as1cm-flutter-local-storage-cache-as2cm-flutter-local-storage-cache-as3