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
Check Flutter installation to setup Flutter
Use flutter create
command to create a Flutter project (here cache_storage_app) :
flutter create cache_storage_app
Add packages (lines 2-7) to pubspec.yaml
dependencies:
state_persistence:
flutter_cache_manager:
html: ^0.14.0+2
http:
path:
path_provider:
flutter:
sdk: flutter
flutter_cache_manager
, path
and path_provider
are used to implement BaseCacheManager
html
and http
packages are specific to provided example, used to retrieve and parse web pages state_persistence
is also specific to example and is used to store selected tab index(of a TabBar used in example) using json storage (check app with state persistence) Run following command to add dependency
flutter pub get
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
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);
}
}
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()
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()}");
});
ImageCacheManager().getSingleFile(image_url).listen((fileInfo) {
image_url = fileInfo?.file?.path;
})
.asFuture(() {}).then((e) {})
.timeout(Duration(seconds:5), onTimeout: () {})
.catchError((e) {});
await ImageCacheManager().removeFile(image_url);
ImageCacheManager().emptyCache();
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);
}
}
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