From 95d642176697f32a7a1e981801a980fdda58c024 Mon Sep 17 00:00:00 2001 From: wauplin Date: Thu, 18 Mar 2021 17:53:48 +0100 Subject: [PATCH 1/5] Universal memory cache for web platform --- README.md | 24 ++- lib/src/cache_manager/abstract.dart | 47 +++++ lib/src/cache_manager/memory_cache.dart | 58 ++++++ .../sqflite_cache.dart} | 167 ++++++++---------- lib/src/cache_manager/universal.dart | 2 + lib/src/firebase_image.dart | 10 +- 6 files changed, 210 insertions(+), 98 deletions(-) create mode 100644 lib/src/cache_manager/abstract.dart create mode 100644 lib/src/cache_manager/memory_cache.dart rename lib/src/{cache_manager.dart => cache_manager/sqflite_cache.dart} (62%) create mode 100644 lib/src/cache_manager/universal.dart diff --git a/README.md b/README.md index 9c6de34..8dcc585 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ +# Why a fork ? + +This fork is meant to propose a solution to use FirebaseImage plugin on any platform, even if `sqflite` and `dart:io` are not available. In particular, it is now possible to use this plugin on the Web. + +:warning: Warnings: + +- This is not a maintained nor a tested fork. I use it only for a hobby project running on both Web and Android platforms. +- On `Web` platform, the "cache" is done in memory. This is an important limitation since memory is neither shared between tabs nor persistent. + +## Fork implementation + +I'm a complete beginner with Flutter/Dart so I went for the simplest solution here. I used [this blog article](https://medium.com/@rody.davis.jr/how-to-build-a-native-cross-platform-project-with-flutter-372b9e4b504f) as inspiration. + +Main ideas: + +- The `FirebaseImageCacheManager` class is abstracted by `AbstractedFirebaseImageCacheManager`. +- The sqflite-based `FirebaseImageCacheManager` is kept as it is (few minor naming changes). +- A memory-based `FirebaseImageCacheManager` is implemented using a simple `Map` object. +- Depending on the presence of `sqflite` dart package, the right cache manager is exported. + # 🔥 Firebase Image Provider [![pub package](https://img.shields.io/pub/v/firebase_image.svg)](https://pub.dartlang.org/packages/firebase_image) - A cached Flutter ImageProvider for Firebase Cloud Storage image objects. ## How to use @@ -14,6 +33,7 @@ Supply the `FirebaseImage` widget with the image's URI (e.g. `gs://bucket123/use See the below for example code. ## How does it work? + The code downloads the image (object) into memory as a byte array. Unless disabled using the `cacheRefreshStrategy: CacheRefreshStrategy.NEVER` option, it gets the object's last update time from metadata (a millisecond precision integer timstamp) and uses that as a defacto version number. Therefore, any update to that remote object will result in the new version being downloaded. @@ -23,6 +43,7 @@ The image byte array in memory then gets saved to a file in the temporary direct Metadata retrival is a 'Class B Operation' and has 50,000 free operations per month. After that, it is billed at $0.04 / 100,000 operations and so the default behaviour of `cacheRefreshStrategy: CacheRefreshStrategy.BY_METADATA_DATE` may incur extra cost if the object never changes. This makes this implementation a cost effective stratergy for caching as the entire object doesn't have to be transfered just to check if there have been any updates. Essentailly, any images will only need to be downloaded once per device. ## Example + ```dart import 'package:flutter/material.dart'; import 'package:firebase_image/firebase_image.dart'; @@ -54,6 +75,7 @@ class IconImage extends StatelessWidget { - [ ] Create unit tests ## Contributing + If you want to contribute, please fork the project and play around there! If you're stuck for ideas, check [Issues](https://github.com/mattreid1/firebase_image/issues) or the above To Do list for inspiration. diff --git a/lib/src/cache_manager/abstract.dart b/lib/src/cache_manager/abstract.dart new file mode 100644 index 0000000..8e2015d --- /dev/null +++ b/lib/src/cache_manager/abstract.dart @@ -0,0 +1,47 @@ +import 'dart:typed_data'; + +import 'package:firebase_image/firebase_image.dart'; +import 'package:firebase_image/src/firebase_image.dart'; +import 'package:firebase_image/src/image_object.dart'; + +abstract class AbstractFirebaseImageCacheManager { + final CacheRefreshStrategy cacheRefreshStrategy; + + AbstractFirebaseImageCacheManager( + this.cacheRefreshStrategy, + ); + + Future open() async {} + + Future close() async {} + + Future getObject( + String uri, FirebaseImage image) async {} + + Future getLocalFileBytes(FirebaseImageObject? object) async {} + + Future getRemoteFileBytes( + FirebaseImageObject object, int maxSizeBytes) { + return object.reference.getData(maxSizeBytes); + } + + Future upsertRemoteFileToCache( + FirebaseImageObject object, int maxSizeBytes) async {} + + Future getRemoteVersion( + FirebaseImageObject object, int defaultValue) async { + return (await object.reference.getMetadata()) + .updated + ?.millisecondsSinceEpoch ?? + defaultValue; + } + + Future checkForUpdate( + FirebaseImageObject object, FirebaseImage image) async { + int remoteVersion = await getRemoteVersion(object, -1); + if (remoteVersion != object.version) { + // If true, download new image for next load + await this.upsertRemoteFileToCache(object, image.maxSizeBytes); + } + } +} diff --git a/lib/src/cache_manager/memory_cache.dart b/lib/src/cache_manager/memory_cache.dart new file mode 100644 index 0000000..6f77ad7 --- /dev/null +++ b/lib/src/cache_manager/memory_cache.dart @@ -0,0 +1,58 @@ +import 'dart:typed_data'; + +import 'package:firebase_image/firebase_image.dart'; +import 'package:firebase_image/src/cache_manager/abstract.dart'; +import 'package:firebase_image/src/firebase_image.dart'; +import 'package:firebase_image/src/image_object.dart'; + +class MemoryCacheEntry { + /// Model for an entry in the memory cache + FirebaseImageObject object; + Uint8List? bytes; + + MemoryCacheEntry({ + required this.object, + required this.bytes, + }); +} + +class FirebaseImageCacheManager extends AbstractFirebaseImageCacheManager { + /// Key is the URI of the image + final memoryCache = Map(); + + FirebaseImageCacheManager(cacheRefreshStrategy) : super(cacheRefreshStrategy); + + // Interface methods + + Future getObject( + String uri, FirebaseImage image) async { + final cacheEntry = memoryCache[uri]; + if (cacheEntry == null) { + return null; + } else { + final returnObject = cacheEntry.object; + if (CacheRefreshStrategy.BY_METADATA_DATE == this.cacheRefreshStrategy) { + checkForUpdate(returnObject, image); // Check for update in background + } + return returnObject; + } + } + + Future getLocalFileBytes(FirebaseImageObject? object) async { + final cacheEntry = memoryCache[object?.uri]; + return cacheEntry?.bytes; + } + + Future upsertRemoteFileToCache( + FirebaseImageObject object, int maxSizeBytes) async { + if (CacheRefreshStrategy.BY_METADATA_DATE == this.cacheRefreshStrategy) { + object.version = await getRemoteVersion(object, 0); + } + Uint8List? bytes = await getRemoteFileBytes(object, maxSizeBytes); + + // "store" bytes in memory + memoryCache[object.uri] = MemoryCacheEntry(object: object, bytes: bytes); + + return bytes; + } +} diff --git a/lib/src/cache_manager.dart b/lib/src/cache_manager/sqflite_cache.dart similarity index 62% rename from lib/src/cache_manager.dart rename to lib/src/cache_manager/sqflite_cache.dart index 7c3a0e3..2458bd2 100644 --- a/lib/src/cache_manager.dart +++ b/lib/src/cache_manager/sqflite_cache.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_image/firebase_image.dart'; +import 'package:firebase_image/src/cache_manager/abstract.dart'; import 'package:firebase_image/src/firebase_image.dart'; import 'package:firebase_image/src/image_object.dart'; import 'package:firebase_storage/firebase_storage.dart'; @@ -10,7 +11,7 @@ import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; -class FirebaseImageCacheManager { +class FirebaseImageCacheManager extends AbstractFirebaseImageCacheManager { static const String key = 'firebase_image'; late Database db; @@ -18,11 +19,9 @@ class FirebaseImageCacheManager { static const String table = 'images'; late String basePath; - final CacheRefreshStrategy cacheRefreshStrategy; + FirebaseImageCacheManager(cacheRefreshStrategy) : super(cacheRefreshStrategy); - FirebaseImageCacheManager( - this.cacheRefreshStrategy, - ); + // Interface methods Future open() async { db = await openDatabase( @@ -40,43 +39,13 @@ class FirebaseImageCacheManager { }, version: 1, ); - basePath = await _createFilePath(); + basePath = await _fileCreatePath(); } - Future insert(FirebaseImageObject model) async { - await db.insert(table, model.toMap()); - return model; - } - - Future update(FirebaseImageObject model) async { - await db.update( - table, - model.toMap(), - where: 'uri = ?', - whereArgs: [model.uri], - ); - return model; - } - - Future upsert(FirebaseImageObject object) async { - if (await checkDatabaseForEntry(object)) { - return await update(object); - } else { - return await insert(object); - } - } - - Future checkDatabaseForEntry(FirebaseImageObject object) async { - final List> maps = await db.query( - table, - columns: const ['uri'], - where: 'uri = ?', - whereArgs: [object.uri], - ); - return maps.length > 0; - } + Future close() => db.close(); - Future get(String uri, FirebaseImage image) async { + Future getObject( + String uri, FirebaseImage image) async { final List> maps = await db.query( table, columns: const [ @@ -91,7 +60,7 @@ class FirebaseImageCacheManager { if (maps.length > 0) { FirebaseImageObject returnObject = FirebaseImageObject.fromMap(maps.first); - returnObject.reference = getImageRef(returnObject, image.firebaseApp); + returnObject.reference = _getImageRef(returnObject, image.firebaseApp); if (CacheRefreshStrategy.BY_METADATA_DATE == this.cacheRefreshStrategy) { checkForUpdate(returnObject, image); // Check for update in background } @@ -100,51 +69,13 @@ class FirebaseImageCacheManager { return null; } - Reference getImageRef(FirebaseImageObject object, FirebaseApp? firebaseApp) { - FirebaseStorage storage = - FirebaseStorage.instanceFor(app: firebaseApp, bucket: object.bucket); - return storage.ref().child(object.remotePath); - } - - Future checkForUpdate( - FirebaseImageObject object, FirebaseImage image) async { - int remoteVersion = (await object.reference.getMetadata()) - .updated - ?.millisecondsSinceEpoch ?? - -1; - if (remoteVersion != object.version) { - // If true, download new image for next load - await this.upsertRemoteFileToCache(object, image.maxSizeBytes); - } - } - - Future> getAll() async { - final List> maps = await db.query(table); - return List.generate(maps.length, (i) { - return FirebaseImageObject.fromMap(maps[i]); - }); - } - - Future delete(String uri) async { - return await db.delete( - table, - where: 'uri = ?', - whereArgs: [uri], - ); - } - - Future localFileBytes(FirebaseImageObject? object) async { + Future getLocalFileBytes(FirebaseImageObject? object) async { if (await _fileExists(object)) { return File(object!.localPath!).readAsBytes(); } return null; } - Future remoteFileBytes( - FirebaseImageObject object, int maxSizeBytes) { - return object.reference.getData(maxSizeBytes); - } - Future upsertRemoteFileToCache( FirebaseImageObject object, int maxSizeBytes) async { if (CacheRefreshStrategy.BY_METADATA_DATE == this.cacheRefreshStrategy) { @@ -153,12 +84,34 @@ class FirebaseImageCacheManager { ?.millisecondsSinceEpoch ?? 0; } - Uint8List? bytes = await remoteFileBytes(object, maxSizeBytes); - await putFile(object, bytes); + Uint8List? bytes = await getRemoteFileBytes(object, maxSizeBytes); + await _filePut(object, bytes); return bytes; } - Future putFile( + // Firestore&-related methods + + Reference _getImageRef(FirebaseImageObject object, FirebaseApp? firebaseApp) { + FirebaseStorage storage = + FirebaseStorage.instanceFor(app: firebaseApp, bucket: object.bucket); + return storage.ref().child(object.remotePath); + } + + // Filesystem-related methods + + Future _fileCreatePath() async { + final directory = await getTemporaryDirectory(); + return join(directory.path, key); + } + + Future _fileExists(FirebaseImageObject? object) async { + if (object?.localPath == null) { + return false; + } + return File(object!.localPath!).exists(); + } + + Future _filePut( FirebaseImageObject object, final bytes) async { String path = basePath + "/" + object.remotePath; path = path.replaceAll("//", "/"); @@ -166,20 +119,50 @@ class FirebaseImageCacheManager { final localFile = await File(path).create(recursive: true); await localFile.writeAsBytes(bytes); object.localPath = localFile.path; - return await upsert(object); + return await _dbUpsert(object); } - Future _fileExists(FirebaseImageObject? object) async { - if (object?.localPath == null) { - return false; - } - return File(object!.localPath!).exists(); + // DB-related methods + + Future _dbCheckForEntry(FirebaseImageObject object) async { + final List> maps = await db.query( + table, + columns: const ['uri'], + where: 'uri = ?', + whereArgs: [object.uri], + ); + return maps.length > 0; } - Future _createFilePath() async { - final directory = await getTemporaryDirectory(); - return join(directory.path, key); + Future _dbInsert(FirebaseImageObject model) async { + await db.insert(table, model.toMap()); + return model; } - Future close() => db.close(); + Future _dbUpdate(FirebaseImageObject model) async { + await db.update( + table, + model.toMap(), + where: 'uri = ?', + whereArgs: [model.uri], + ); + return model; + } + + Future _dbUpsert(FirebaseImageObject object) async { + if (await _dbCheckForEntry(object)) { + return await _dbUpdate(object); + } else { + return await _dbInsert(object); + } + } + + // DB delete currently not in use + // Future _dbDelete(String uri) async { + // return await db.delete( + // table, + // where: 'uri = ?', + // whereArgs: [uri], + // ); + // } } diff --git a/lib/src/cache_manager/universal.dart b/lib/src/cache_manager/universal.dart new file mode 100644 index 0000000..aaf8e15 --- /dev/null +++ b/lib/src/cache_manager/universal.dart @@ -0,0 +1,2 @@ +export 'package:firebase_image/src/cache_manager/memory_cache.dart' + if (dart.library.io) 'package:firebase_image/src/cache_manager/sqflite_cache.dart'; diff --git a/lib/src/firebase_image.dart b/lib/src/firebase_image.dart index 5526269..cf0d123 100644 --- a/lib/src/firebase_image.dart +++ b/lib/src/firebase_image.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_image/firebase_image.dart'; -import 'package:firebase_image/src/cache_manager.dart'; +import 'package:firebase_image/src/cache_manager/universal.dart'; import 'package:firebase_image/src/image_object.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/foundation.dart'; @@ -79,10 +79,10 @@ class FirebaseImage extends ImageProvider { if (shouldCache) { await cacheManager.open(); FirebaseImageObject? localObject = - await cacheManager.get(_imageObject.uri, this); + await cacheManager.getObject(_imageObject.uri, this); if (localObject != null) { - bytes = await cacheManager.localFileBytes(localObject); + bytes = await cacheManager.getLocalFileBytes(localObject); if (bytes == null) { bytes = await cacheManager.upsertRemoteFileToCache( _imageObject, this.maxSizeBytes); @@ -92,8 +92,8 @@ class FirebaseImage extends ImageProvider { _imageObject, this.maxSizeBytes); } } else { - bytes = - await cacheManager.remoteFileBytes(_imageObject, this.maxSizeBytes); + bytes = await cacheManager.getRemoteFileBytes( + _imageObject, this.maxSizeBytes); } return bytes!; From ae885b2210297c0130c9a140ef11ee1cae2d04ec Mon Sep 17 00:00:00 2001 From: wauplin Date: Thu, 18 Mar 2021 18:10:25 +0100 Subject: [PATCH 2/5] add getAll everywhere --- lib/src/cache_manager/abstract.dart | 20 +++++++++++++++----- lib/src/cache_manager/memory_cache.dart | 6 ++++++ lib/src/cache_manager/sqflite_cache.dart | 7 +++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/src/cache_manager/abstract.dart b/lib/src/cache_manager/abstract.dart index 8e2015d..32900d0 100644 --- a/lib/src/cache_manager/abstract.dart +++ b/lib/src/cache_manager/abstract.dart @@ -16,18 +16,28 @@ abstract class AbstractFirebaseImageCacheManager { Future close() async {} Future getObject( - String uri, FirebaseImage image) async {} + String uri, FirebaseImage image) async { + throw UnimplementedError(); + } - Future getLocalFileBytes(FirebaseImageObject? object) async {} + Future> getAllObjects() async { + throw UnimplementedError(); + } + + Future getLocalFileBytes(FirebaseImageObject? object) async { + throw UnimplementedError(); + } + + Future upsertRemoteFileToCache( + FirebaseImageObject object, int maxSizeBytes) async { + throw UnimplementedError(); + } Future getRemoteFileBytes( FirebaseImageObject object, int maxSizeBytes) { return object.reference.getData(maxSizeBytes); } - Future upsertRemoteFileToCache( - FirebaseImageObject object, int maxSizeBytes) async {} - Future getRemoteVersion( FirebaseImageObject object, int defaultValue) async { return (await object.reference.getMetadata()) diff --git a/lib/src/cache_manager/memory_cache.dart b/lib/src/cache_manager/memory_cache.dart index 6f77ad7..a6f4203 100644 --- a/lib/src/cache_manager/memory_cache.dart +++ b/lib/src/cache_manager/memory_cache.dart @@ -38,6 +38,12 @@ class FirebaseImageCacheManager extends AbstractFirebaseImageCacheManager { } } + Future> getAllObjects() async { + final List objects = []; + memoryCache.forEach((k, v) => objects.add(v.object)); + return objects; + } + Future getLocalFileBytes(FirebaseImageObject? object) async { final cacheEntry = memoryCache[object?.uri]; return cacheEntry?.bytes; diff --git a/lib/src/cache_manager/sqflite_cache.dart b/lib/src/cache_manager/sqflite_cache.dart index 2458bd2..54f3921 100644 --- a/lib/src/cache_manager/sqflite_cache.dart +++ b/lib/src/cache_manager/sqflite_cache.dart @@ -69,6 +69,13 @@ class FirebaseImageCacheManager extends AbstractFirebaseImageCacheManager { return null; } + Future> getAllObjects() async { + final List> maps = await db.query(table); + return List.generate(maps.length, (i) { + return FirebaseImageObject.fromMap(maps[i]); + }); + } + Future getLocalFileBytes(FirebaseImageObject? object) async { if (await _fileExists(object)) { return File(object!.localPath!).readAsBytes(); From 19214fa902e276eae031879cd8f18e68166f429a Mon Sep 17 00:00:00 2001 From: wauplin Date: Fri, 9 Apr 2021 14:30:26 +0200 Subject: [PATCH 3/5] Dirty placeholder in case image is not fetched --- lib/src/firebase_image.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/src/firebase_image.dart b/lib/src/firebase_image.dart index cf0d123..32f224f 100644 --- a/lib/src/firebase_image.dart +++ b/lib/src/firebase_image.dart @@ -16,6 +16,9 @@ class FirebaseImage extends ImageProvider { /// Default: 1.0. The scale to display the image at (optional) final double scale; + // Default null. What to return if no bytes are returned (example: wrong uri, image too large,...) + final Uint8List? defaultBytes; + /// Default: 2.5MB. The maximum size in bytes to be allocated in the device's memory for the image (optional) final int maxSizeBytes; @@ -33,6 +36,7 @@ class FirebaseImage extends ImageProvider { /// [location] The URI of the image, in the bucket, to be displayed /// [shouldCache] Default: True. Specified whether or not an image should be cached (optional) /// [scale] Default: 1.0. The scale to display the image at (optional) + /// [defaultBytes] Default: null. The bytes to return if no image is fetched. /// [maxSizeBytes] Default: 2.5MB. The maximum size in bytes to be allocated in the device's memory for the image (optional) /// [cacheRefreshStrategy] Default: BY_METADATA_DATE. Specifies the strategy in which to check if the cached version should be refreshed (optional) /// [firebaseApp] Default: the default Firebase app. Specifies a custom Firebase app to make the request to the bucket from (optional) @@ -40,6 +44,7 @@ class FirebaseImage extends ImageProvider { String location, { this.shouldCache = true, this.scale = 1.0, + this.defaultBytes, this.maxSizeBytes = 2500 * 1000, // 2.5MB this.cacheRefreshStrategy = CacheRefreshStrategy.BY_METADATA_DATE, this.firebaseApp, @@ -49,11 +54,6 @@ class FirebaseImage extends ImageProvider { reference: _getImageRef(location, firebaseApp), ); - /// Returns the image as bytes - Future getBytes() { - return _fetchImage(); - } - static String _getBucket(String location) { final uri = Uri.parse(location); return '${uri.scheme}://${uri.authority}'; @@ -96,7 +96,11 @@ class FirebaseImage extends ImageProvider { _imageObject, this.maxSizeBytes); } - return bytes!; + if (bytes == null) { + return this.defaultBytes!; + } + + return bytes; } Future _fetchImageCodec() async { From ec5705544169733d89298de6c75fb86393162417 Mon Sep 17 00:00:00 2001 From: wauplin Date: Fri, 23 Apr 2021 12:53:11 +0200 Subject: [PATCH 4/5] Default bytes callback when image not found --- lib/src/firebase_image.dart | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/src/firebase_image.dart b/lib/src/firebase_image.dart index 32f224f..3fccdab 100644 --- a/lib/src/firebase_image.dart +++ b/lib/src/firebase_image.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'dart:ui'; +import 'dart:convert' show base64Decode; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_image/firebase_image.dart'; @@ -9,6 +10,13 @@ import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +final Uint8List emptyImagePlaceholder = base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV/TSv2oONhBxCFDdbIgWsRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC4AeIm5uToouU+L+00CLGg+N+vLv3uHsHCPUy06zABKDptplKxMVMdlUMviKAHvQhhkGZWcacJCXhOb7u4ePrXZRneZ/7c/SrOYsBPpF4lhmmTbxBPL1pG5z3icOsKKvE58TjJl2Q+JHrSpPfOBdcFnhm2Eyn5onDxGKhg5UOZkVTI44RR1RNp3wh02SV8xZnrVxlrXvyF4Zy+soy12mOIIFFLEGCCAVVlFCGjSitOikWUrQf9/APu36JXAq5SmDkWEAFGmTXD/4Hv7u18lOTzaRQHOh6cZyPUSC4CzRqjvN97DiNE8D/DFzpbX+lDsx8kl5ra5EjYGAbuLhua8oecLkDDD0Zsim7kp+mkM8D72f0TVlg8BboXWv21trH6QOQpq6SN8DBITBWoOx1j3d3d/b275lWfz9Z83Kd00lbqwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+UECQs0OeqvXcUAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC'); + +Uint8List defaultCallback() { + return emptyImagePlaceholder; +} + class FirebaseImage extends ImageProvider { // Default: True. Specified whether or not an image should be cached (optional) final bool shouldCache; @@ -17,7 +25,7 @@ class FirebaseImage extends ImageProvider { final double scale; // Default null. What to return if no bytes are returned (example: wrong uri, image too large,...) - final Uint8List? defaultBytes; + final Uint8List Function() getDefaultBytesCallback; /// Default: 2.5MB. The maximum size in bytes to be allocated in the device's memory for the image (optional) final int maxSizeBytes; @@ -34,9 +42,9 @@ class FirebaseImage extends ImageProvider { /// Fetches, saves and returns an ImageProvider for any image in a readable Firebase Cloud Storeage bucket. /// /// [location] The URI of the image, in the bucket, to be displayed + /// [getDefaultBytesCallback] Default: null. The bytes to return if no image is fetched. /// [shouldCache] Default: True. Specified whether or not an image should be cached (optional) /// [scale] Default: 1.0. The scale to display the image at (optional) - /// [defaultBytes] Default: null. The bytes to return if no image is fetched. /// [maxSizeBytes] Default: 2.5MB. The maximum size in bytes to be allocated in the device's memory for the image (optional) /// [cacheRefreshStrategy] Default: BY_METADATA_DATE. Specifies the strategy in which to check if the cached version should be refreshed (optional) /// [firebaseApp] Default: the default Firebase app. Specifies a custom Firebase app to make the request to the bucket from (optional) @@ -44,8 +52,8 @@ class FirebaseImage extends ImageProvider { String location, { this.shouldCache = true, this.scale = 1.0, - this.defaultBytes, this.maxSizeBytes = 2500 * 1000, // 2.5MB + this.getDefaultBytesCallback = defaultCallback, this.cacheRefreshStrategy = CacheRefreshStrategy.BY_METADATA_DATE, this.firebaseApp, }) : _imageObject = FirebaseImageObject( @@ -70,7 +78,7 @@ class FirebaseImage extends ImageProvider { return storage.ref().child(_getImagePath(location)); } - Future _fetchImage() async { + Future _fetchImage() async { Uint8List? bytes; FirebaseImageCacheManager cacheManager = FirebaseImageCacheManager( cacheRefreshStrategy, @@ -96,16 +104,23 @@ class FirebaseImage extends ImageProvider { _imageObject, this.maxSizeBytes); } - if (bytes == null) { - return this.defaultBytes!; - } - return bytes; } + Future _fetchImageOrDefault() async { + Uint8List? bytes; + try { + bytes = await _fetchImage(); + } on FirebaseException catch (e) { + // Image does not exist -> silently catch error + bytes = null; + } + return bytes ?? this.getDefaultBytesCallback(); + } + Future _fetchImageCodec() async { return await PaintingBinding.instance! - .instantiateImageCodec(await _fetchImage()); + .instantiateImageCodec(await _fetchImageOrDefault()); } @override From 552e1dfcbacf45b4026ff48c35ebac3c45fbc68c Mon Sep 17 00:00:00 2001 From: wauplin Date: Fri, 1 Oct 2021 12:41:47 +0200 Subject: [PATCH 5/5] final one ? --- lib/src/cache_manager/sqflite_cache.dart | 10 ---------- lib/src/firebase_image.dart | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/src/cache_manager/sqflite_cache.dart b/lib/src/cache_manager/sqflite_cache.dart index 54f3921..6f22285 100644 --- a/lib/src/cache_manager/sqflite_cache.dart +++ b/lib/src/cache_manager/sqflite_cache.dart @@ -122,7 +122,6 @@ class FirebaseImageCacheManager extends AbstractFirebaseImageCacheManager { FirebaseImageObject object, final bytes) async { String path = basePath + "/" + object.remotePath; path = path.replaceAll("//", "/"); - //print(join(basePath, object.remotePath)); Join isn't working? final localFile = await File(path).create(recursive: true); await localFile.writeAsBytes(bytes); object.localPath = localFile.path; @@ -163,13 +162,4 @@ class FirebaseImageCacheManager extends AbstractFirebaseImageCacheManager { return await _dbInsert(object); } } - - // DB delete currently not in use - // Future _dbDelete(String uri) async { - // return await db.delete( - // table, - // where: 'uri = ?', - // whereArgs: [uri], - // ); - // } } diff --git a/lib/src/firebase_image.dart b/lib/src/firebase_image.dart index 3fccdab..dbc440b 100644 --- a/lib/src/firebase_image.dart +++ b/lib/src/firebase_image.dart @@ -13,10 +13,6 @@ import 'package:flutter/material.dart'; final Uint8List emptyImagePlaceholder = base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV/TSv2oONhBxCFDdbIgWsRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC4AeIm5uToouU+L+00CLGg+N+vLv3uHsHCPUy06zABKDptplKxMVMdlUMviKAHvQhhkGZWcacJCXhOb7u4ePrXZRneZ/7c/SrOYsBPpF4lhmmTbxBPL1pG5z3icOsKKvE58TjJl2Q+JHrSpPfOBdcFnhm2Eyn5onDxGKhg5UOZkVTI44RR1RNp3wh02SV8xZnrVxlrXvyF4Zy+soy12mOIIFFLEGCCAVVlFCGjSitOikWUrQf9/APu36JXAq5SmDkWEAFGmTXD/4Hv7u18lOTzaRQHOh6cZyPUSC4CzRqjvN97DiNE8D/DFzpbX+lDsx8kl5ra5EjYGAbuLhua8oecLkDDD0Zsim7kp+mkM8D72f0TVlg8BboXWv21trH6QOQpq6SN8DBITBWoOx1j3d3d/b275lWfz9Z83Kd00lbqwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+UECQs0OeqvXcUAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC'); -Uint8List defaultCallback() { - return emptyImagePlaceholder; -} - class FirebaseImage extends ImageProvider { // Default: True. Specified whether or not an image should be cached (optional) final bool shouldCache; @@ -25,7 +21,7 @@ class FirebaseImage extends ImageProvider { final double scale; // Default null. What to return if no bytes are returned (example: wrong uri, image too large,...) - final Uint8List Function() getDefaultBytesCallback; + final Uint8List Function()? getDefaultBytesCallback; /// Default: 2.5MB. The maximum size in bytes to be allocated in the device's memory for the image (optional) final int maxSizeBytes; @@ -53,7 +49,7 @@ class FirebaseImage extends ImageProvider { this.shouldCache = true, this.scale = 1.0, this.maxSizeBytes = 2500 * 1000, // 2.5MB - this.getDefaultBytesCallback = defaultCallback, + this.getDefaultBytesCallback, this.cacheRefreshStrategy = CacheRefreshStrategy.BY_METADATA_DATE, this.firebaseApp, }) : _imageObject = FirebaseImageObject( @@ -111,11 +107,11 @@ class FirebaseImage extends ImageProvider { Uint8List? bytes; try { bytes = await _fetchImage(); - } on FirebaseException catch (e) { + } on FirebaseException { // Image does not exist -> silently catch error bytes = null; } - return bytes ?? this.getDefaultBytesCallback(); + return bytes ?? getDefaultBytes(); } Future _fetchImageCodec() async { @@ -150,4 +146,11 @@ class FirebaseImage extends ImageProvider { @override String toString() => '$runtimeType("${_imageObject.uri}", scale: ${this.scale})'; + + Uint8List getDefaultBytes() { + if (this.getDefaultBytesCallback == null) { + return emptyImagePlaceholder; + } + return this.getDefaultBytesCallback!(); + } }