├── .github └── ISSUE_TEMPLATE │ └── bug-report.md ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── firebase_image.dart └── src │ ├── cache_manager.dart │ ├── cache_refresh_strategy.dart │ ├── firebase_image.dart │ └── image_object.dart ├── pubspec.yaml └── test └── firebase_image_test.dart /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a bug report for an issue to be fixed with the code. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Expected Behaviour 11 | *(e.g. Image cached locally)* 12 | 13 | ## Actual Behaviour 14 | *(e.g. App crashed with stack trace provided)* 15 | 16 | ## Steps to Reproduce the Problem 17 | 1. *...* 18 | 1. *...* 19 | 1. *...* 20 | 21 | ## Specifications 22 | - Version: *(e.g. 0.1.4)* 23 | - Device: *(e.g. iPhone 6)* 24 | - Platform: *(e.g. Hummingbird)* 25 | - Flutter Version: *(please enter the output of `flutter --version`)* 26 | 27 | ## Screenshots 28 | *If applicable, please add screenshots to help explain your problem.* 29 | 30 | ## Additional Context 31 | *(optional) Add any other context about the problem here.* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # Ignoring as this is a library (see https://github.com/toptal/gitignore/blob/2d8160c7f607e142929d2ba213620a638e50603f/templates/Flutter.gitignore#L16) 13 | pubspec.lock 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | build/ 34 | 35 | # Android related 36 | **/android/**/gradle-wrapper.jar 37 | **/android/.gradle 38 | **/android/captures/ 39 | **/android/gradlew 40 | **/android/gradlew.bat 41 | **/android/local.properties 42 | **/android/**/GeneratedPluginRegistrant.java 43 | 44 | # iOS/XCode related 45 | **/ios/**/*.mode1v3 46 | **/ios/**/*.mode2v3 47 | **/ios/**/*.moved-aside 48 | **/ios/**/*.pbxuser 49 | **/ios/**/*.perspectivev3 50 | **/ios/**/*sync/ 51 | **/ios/**/.sconsign.dblite 52 | **/ios/**/.tags* 53 | **/ios/**/.vagrant/ 54 | **/ios/**/DerivedData/ 55 | **/ios/**/Icon? 56 | **/ios/**/Pods/ 57 | **/ios/**/.symlinks/ 58 | **/ios/**/profile 59 | **/ios/**/xcuserdata 60 | **/ios/.generated/ 61 | **/ios/Flutter/App.framework 62 | **/ios/Flutter/Flutter.framework 63 | **/ios/Flutter/Generated.xcconfig 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 77 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 2d2a1ffec95cc70a3218872a2cd3f8de4933c42f 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.1.1] 25/12/2021 2 | 3 | - Merged [PR #45](https://github.com/mattreid1/firebase_image/pull/45) for null reference fix 4 | 5 | ## [1.1.0] 23/12/2021 6 | 7 | - Merged [PR #33](https://github.com/mattreid1/firebase_image/pull/33) for pre-cache function 8 | - Updated example/main.dart file 9 | - Formatted code 10 | 11 | ## [1.0.2] 23/12/2021 12 | 13 | - Merged [PR #55](https://github.com/mattreid1/firebase_image/pull/55) to update dependancies 14 | 15 | ## [1.0.1] 12/03/2021 16 | 17 | - Formatted code according to `dartfmt` 18 | 19 | ## [1.0.0] - 09/03/2021 20 | 21 | - 1.0.0 release! 🥳 22 | - Uses Firebase Storage ^8.0.0 and Firebase Core ^1.0.0 23 | - Merged [PR #30](https://github.com/mattreid1/firebase_image/pull/30) to update dependancies and add null saftey 24 | - Merged [PR #28](https://github.com/mattreid1/firebase_image/pull/28) for code style correction 25 | - Merged [PR #24](https://github.com/mattreid1/firebase_image/pull/24) to update dependancies 26 | 27 | ## [0.3.0] - 06/11/2020 28 | 29 | - Merged [PR #20](https://github.com/mattreid1/firebase_image/pull/20) to work with Firebase Storage 5.0.0 30 | - sqflite dependency change 31 | - Minor formatting updates 32 | 33 | ## [0.2.0] - 25/08/2020 34 | 35 | - Merged [PR #16](https://github.com/mattreid1/firebase_image/pull/16) for dependency upgrades. 36 | - Merged [PR #9](https://github.com/mattreid1/firebase_image/pull/9) for general code uppgrades. 37 | 38 | ## [0.1.6] - 10/05/2020 39 | 40 | - Added ability to disable metadata (class B operations) requests for images (e.g. for images that will never change). 41 | 42 | ## [0.1.5] - 26/12/2019 43 | 44 | - Fixes/image provider compilation issue from [Druchinin/fixes/imageProviderCompilationIssue.](https://github.com/mattreid1/firebase_image/pull/3) 45 | 46 | ## [0.1.4] - 27/11/2019 47 | 48 | - Added ability to get image as bytes. 49 | - Formatted test file. 50 | 51 | ## [0.1.3] - 22/10/2019 52 | 53 | - Updated author information to match verified publisher account. 54 | - Formatted code. 55 | 56 | ## [0.1.2] - 22/09/2019 57 | 58 | - Hash code implemented (allows for things like [Hero widgets](https://flutter.dev/docs/development/ui/animations/hero-animations)). 59 | 60 | ## [0.1.1] - 22/09/2019 61 | 62 | - Database primary key bug fix. 63 | - Misc. bug fixes. 64 | 65 | ## [0.1.0] - 22/09/2019 66 | 67 | - Added example. 68 | - Formatted code. 69 | 70 | ## [0.0.1] - 22/09/2019 71 | 72 | - Can download, save and render images from Firebase Cloud Storage. 73 | - Basic caching system works but no object expiration time or read monitoring as of yet. 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Matthew Reid (mattreid1) and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔥 Firebase Image Provider 2 | 3 | [![pub package](https://img.shields.io/pub/v/firebase_image.svg)](https://pub.dartlang.org/packages/firebase_image) 4 | 5 | 6 | A cached Flutter ImageProvider for Firebase Cloud Storage image objects. 7 | 8 | ## How to use 9 | 10 | Make sure you already have [Firebase set up](https://firebase.google.com/docs/flutter/setup) on all platforms you want to use this on. 11 | 12 | Supply the `FirebaseImage` widget with the image's URI (e.g. `gs://bucket123/userIcon123.jpg`) and then put that in any widget that accepts an `ImageProvider` (most image related widgets will (e.g. `Image`, `ImageIcon`, etc.)). Please note that you do need the `gs://` prefix currently. 13 | 14 | See the below for example code. 15 | 16 | ## How does it work? 17 | The code downloads the image (object) into memory as a byte array. 18 | 19 | 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. 20 | 21 | The image byte array in memory then gets saved to a file in the temporary directory of the app and that location is saved in a persistant database. The OS can clean up this directory at any time however. 22 | 23 | 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. 24 | 25 | ## Example 26 | ```dart 27 | import 'package:flutter/material.dart'; 28 | import 'package:firebase_image/firebase_image.dart'; 29 | 30 | class IconImage extends StatelessWidget { 31 | @override 32 | Widget build(BuildContext context) { 33 | return Scaffold( 34 | appBar: AppBar( 35 | title: Text('Firebase Image Provider example'), 36 | ), 37 | body: Image( 38 | image: FirebaseImage('gs://bucket123/userIcon123.jpg'), 39 | // Works with standard parameters, e.g. 40 | fit: BoxFit.fitWidth, 41 | width: 100, 42 | // ... etc. 43 | ), 44 | ); 45 | } 46 | } 47 | ``` 48 | 49 | ## To Do 50 | 51 | - [x] Add examples to [pub.dev](https://pub.dartlang.org/packages/firebase_image#-example-tab-) 52 | - [ ] Clear items from cache if they haven't been accessed after a certain amount of time (2 weeks?) 53 | - [ ] Add more documentation/comments 54 | - [ ] Create unit tests 55 | 56 | ## Contributing 57 | If you want to contribute, please fork the project and play around there! 58 | 59 | If you're stuck for ideas, check [Issues](https://github.com/mattreid1/firebase_image/issues) or the above To Do list for inspiration. 60 | 61 | Please check PRs and other peoples forks to see if anyone is working on something similiar to what you want to do. 62 | 63 | Once you're ready, please submit a pull request. 64 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:firebase_image/firebase_image.dart'; 3 | import 'package:firebase_core/firebase_core.dart'; 4 | 5 | void main() async { 6 | WidgetsFlutterBinding.ensureInitialized(); 7 | await Firebase.initializeApp(); 8 | runApp(const MyApp()); 9 | } 10 | 11 | class MyApp extends StatelessWidget { 12 | const MyApp({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return MaterialApp( 17 | title: 'Firebase Image Provider Demo', 18 | theme: ThemeData( 19 | primarySwatch: Colors.blue, 20 | ), 21 | home: const MyHomePage(title: 'Firebase Image Provider example'), 22 | ); 23 | } 24 | } 25 | 26 | class MyHomePage extends StatefulWidget { 27 | const MyHomePage({Key? key, required this.title}) : super(key: key); 28 | 29 | final String title; 30 | 31 | @override 32 | _MyHomePageState createState() => _MyHomePageState(); 33 | } 34 | 35 | class _MyHomePageState extends State { 36 | @override 37 | void initState() { 38 | super.initState(); 39 | FirebaseImage('gs://bucket123/otherUser123.jpg').preCache(); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Scaffold( 45 | appBar: AppBar( 46 | title: Text(widget.title), 47 | ), 48 | body: Image( 49 | image: FirebaseImage('gs://bucket123/userIcon123.jpg', 50 | shouldCache: true, // The image should be cached (default: True) 51 | maxSizeBytes: 3000 * 1000, // 3MB max file size (default: 2.5MB) 52 | cacheRefreshStrategy: 53 | CacheRefreshStrategy.NEVER // Switch off update checking 54 | ), 55 | width: 100, 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/firebase_image.dart: -------------------------------------------------------------------------------- 1 | library firebase_image; 2 | 3 | export 'src/firebase_image.dart'; 4 | export 'src/cache_refresh_strategy.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/cache_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:firebase_image/firebase_image.dart'; 5 | import 'package:firebase_image/src/image_object.dart'; 6 | import 'package:path/path.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | import 'package:sqflite/sqflite.dart'; 9 | 10 | class FirebaseImageCacheManager { 11 | static const String key = 'firebase_image'; 12 | 13 | late Database db; 14 | static const String dbName = '$key.db'; 15 | static const String table = 'images'; 16 | late String basePath; 17 | 18 | final CacheRefreshStrategy cacheRefreshStrategy; 19 | 20 | FirebaseImageCacheManager( 21 | this.cacheRefreshStrategy, 22 | ); 23 | 24 | Future open() async { 25 | db = await openDatabase( 26 | join((await getDatabasesPath()), dbName), 27 | onCreate: (Database db, int version) async { 28 | await db.execute(''' 29 | CREATE TABLE $table ( 30 | uri TEXT PRIMARY KEY, 31 | remotePath TEXT, 32 | localPath TEXT, 33 | bucket TEXT, 34 | version INTEGER 35 | ) 36 | '''); 37 | }, 38 | version: 1, 39 | ); 40 | basePath = await _createFilePath(); 41 | } 42 | 43 | Future insert(FirebaseImageObject model) async { 44 | await db.insert(table, model.toMap()); 45 | return model; 46 | } 47 | 48 | Future update(FirebaseImageObject model) async { 49 | await db.update( 50 | table, 51 | model.toMap(), 52 | where: 'uri = ?', 53 | whereArgs: [model.uri], 54 | ); 55 | return model; 56 | } 57 | 58 | Future upsert(FirebaseImageObject object) async { 59 | if (await checkDatabaseForEntry(object)) { 60 | return await update(object); 61 | } else { 62 | return await insert(object); 63 | } 64 | } 65 | 66 | Future checkDatabaseForEntry(FirebaseImageObject object) async { 67 | final List> maps = await db.query( 68 | table, 69 | columns: const ['uri'], 70 | where: 'uri = ?', 71 | whereArgs: [object.uri], 72 | ); 73 | return maps.isNotEmpty; 74 | } 75 | 76 | Future get(String uri, FirebaseImage image) async { 77 | final List> maps = await db.query( 78 | table, 79 | columns: const [ 80 | 'remotePath', 81 | 'localPath', 82 | 'bucket', 83 | 'version', 84 | ], 85 | where: 'uri = ?', 86 | whereArgs: [uri], 87 | ); 88 | if (maps.isNotEmpty) { 89 | FirebaseImageObject returnObject = 90 | FirebaseImageObject.fromMap(maps.first, image.firebaseApp); 91 | if (CacheRefreshStrategy.BY_METADATA_DATE == cacheRefreshStrategy) { 92 | checkForUpdate(returnObject, image); // Check for update in background 93 | } 94 | return returnObject; 95 | } 96 | return null; 97 | } 98 | 99 | Future checkForUpdate( 100 | FirebaseImageObject object, FirebaseImage image) async { 101 | int remoteVersion = (await object.reference.getMetadata()) 102 | .updated 103 | ?.millisecondsSinceEpoch ?? 104 | -1; 105 | if (remoteVersion != object.version) { 106 | // If true, download new image for next load 107 | await upsertRemoteFileToCache(object, image.maxSizeBytes); 108 | } 109 | } 110 | 111 | Future> getAll() async { 112 | final List> maps = await db.query(table); 113 | return List.generate(maps.length, (i) { 114 | return FirebaseImageObject.fromMap(maps[i]); 115 | }); 116 | } 117 | 118 | Future delete(String uri) async { 119 | return await db.delete( 120 | table, 121 | where: 'uri = ?', 122 | whereArgs: [uri], 123 | ); 124 | } 125 | 126 | Future localFileBytes(FirebaseImageObject? object) async { 127 | if (await _fileExists(object)) { 128 | return File(object!.localPath!).readAsBytes(); 129 | } 130 | return null; 131 | } 132 | 133 | Future remoteFileBytes( 134 | FirebaseImageObject object, int maxSizeBytes) { 135 | return object.reference.getData(maxSizeBytes); 136 | } 137 | 138 | Future upsertRemoteFileToCache( 139 | FirebaseImageObject object, int maxSizeBytes) async { 140 | if (CacheRefreshStrategy.BY_METADATA_DATE == cacheRefreshStrategy) { 141 | object.version = (await object.reference.getMetadata()) 142 | .updated 143 | ?.millisecondsSinceEpoch ?? 144 | 0; 145 | } 146 | Uint8List? bytes = await remoteFileBytes(object, maxSizeBytes); 147 | await putFile(object, bytes); 148 | return bytes; 149 | } 150 | 151 | Future putFile( 152 | FirebaseImageObject object, final bytes) async { 153 | String path = basePath + "/" + object.remotePath; 154 | path = path.replaceAll("//", "/"); 155 | //print(join(basePath, object.remotePath)); Join isn't working? 156 | final localFile = await File(path).create(recursive: true); 157 | await localFile.writeAsBytes(bytes); 158 | object.localPath = localFile.path; 159 | return await upsert(object); 160 | } 161 | 162 | Future _fileExists(FirebaseImageObject? object) async { 163 | if (object?.localPath == null) { 164 | return false; 165 | } 166 | return File(object!.localPath!).exists(); 167 | } 168 | 169 | Future _createFilePath() async { 170 | final directory = await getTemporaryDirectory(); 171 | return join(directory.path, key); 172 | } 173 | 174 | Future close() => db.close(); 175 | } 176 | -------------------------------------------------------------------------------- /lib/src/cache_refresh_strategy.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | enum CacheRefreshStrategy { 3 | // BY_METADATA_DATE uses the Storage Object updated timestamp as a version 4 | // number and checks for updates every time. 5 | BY_METADATA_DATE, 6 | // NEVER will never check for any updates. It will still reload a previously- 7 | // cached version that has been cleaned up by the OS. 8 | NEVER, 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/firebase_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:ui'; 3 | 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:firebase_image/firebase_image.dart'; 6 | import 'package:firebase_image/src/cache_manager.dart'; 7 | import 'package:firebase_image/src/image_object.dart'; 8 | import 'package:firebase_storage/firebase_storage.dart'; 9 | import 'package:flutter/foundation.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class FirebaseImage extends ImageProvider { 13 | // Default: True. Specified whether or not an image should be cached (optional) 14 | final bool shouldCache; 15 | 16 | /// Default: 1.0. The scale to display the image at (optional) 17 | final double scale; 18 | 19 | /// Default: 2.5MB. The maximum size in bytes to be allocated in the device's memory for the image (optional) 20 | final int maxSizeBytes; 21 | 22 | /// Default: BY_METADATA_DATE. Specifies the strategy in which to check if the cached version should be refreshed (optional) 23 | final CacheRefreshStrategy cacheRefreshStrategy; 24 | 25 | /// Default: the default Firebase app. Specifies a custom Firebase app to make the request to the bucket from (optional) 26 | final FirebaseApp? firebaseApp; 27 | 28 | /// The model for the image object 29 | final FirebaseImageObject _imageObject; 30 | 31 | /// Fetches, saves and returns an ImageProvider for any image in a readable Firebase Cloud Storeage bucket. 32 | /// 33 | /// [location] The URI of the image, in the bucket, to be displayed 34 | /// [shouldCache] Default: True. Specified whether or not an image should be cached (optional) 35 | /// [scale] Default: 1.0. The scale to display the image at (optional) 36 | /// [maxSizeBytes] Default: 2.5MB. The maximum size in bytes to be allocated in the device's memory for the image (optional) 37 | /// [cacheRefreshStrategy] Default: BY_METADATA_DATE. Specifies the strategy in which to check if the cached version should be refreshed (optional) 38 | /// [firebaseApp] Default: the default Firebase app. Specifies a custom Firebase app to make the request to the bucket from (optional) 39 | FirebaseImage( 40 | String location, { 41 | this.shouldCache = true, 42 | this.scale = 1.0, 43 | this.maxSizeBytes = 2500 * 1000, // 2.5MB 44 | this.cacheRefreshStrategy = CacheRefreshStrategy.BY_METADATA_DATE, 45 | this.firebaseApp, 46 | }) : _imageObject = FirebaseImageObject( 47 | bucket: _getBucket(location), 48 | remotePath: _getImagePath(location), 49 | reference: _getImageRef(location, firebaseApp), 50 | ); 51 | 52 | /// Returns the image as bytes 53 | Future getBytes() { 54 | return _fetchImage(); 55 | } 56 | 57 | /// Pre-caches an image 58 | Future preCache() async { 59 | if (shouldCache == false) { 60 | throw "Caching must be enabled to pre-cache an image."; 61 | } 62 | await _fetchImage(); 63 | } 64 | 65 | static String _getBucket(String location) { 66 | final uri = Uri.parse(location); 67 | return '${uri.scheme}://${uri.authority}'; 68 | } 69 | 70 | static String _getImagePath(String location) { 71 | final uri = Uri.parse(location); 72 | return uri.path; 73 | } 74 | 75 | static Reference _getImageRef(String location, FirebaseApp? firebaseApp) { 76 | FirebaseStorage storage = FirebaseStorage.instanceFor( 77 | app: firebaseApp, bucket: _getBucket(location)); 78 | return storage.ref().child(_getImagePath(location)); 79 | } 80 | 81 | Future _fetchImage() async { 82 | Uint8List? bytes; 83 | FirebaseImageCacheManager cacheManager = FirebaseImageCacheManager( 84 | cacheRefreshStrategy, 85 | ); 86 | 87 | if (shouldCache) { 88 | await cacheManager.open(); 89 | FirebaseImageObject? localObject = 90 | await cacheManager.get(_imageObject.uri, this); 91 | 92 | if (localObject != null) { 93 | bytes = await cacheManager.localFileBytes(localObject); 94 | bytes ??= await cacheManager.upsertRemoteFileToCache( 95 | _imageObject, maxSizeBytes); 96 | } else { 97 | bytes = await cacheManager.upsertRemoteFileToCache( 98 | _imageObject, maxSizeBytes); 99 | } 100 | } else { 101 | bytes = await cacheManager.remoteFileBytes(_imageObject, maxSizeBytes); 102 | } 103 | 104 | return bytes!; 105 | } 106 | 107 | Future _fetchImageCodec() async { 108 | return await PaintingBinding.instance! 109 | .instantiateImageCodec(await _fetchImage()); 110 | } 111 | 112 | @override 113 | Future obtainKey(ImageConfiguration configuration) { 114 | return SynchronousFuture(this); 115 | } 116 | 117 | @override 118 | ImageStreamCompleter load(FirebaseImage key, DecoderCallback decode) { 119 | return MultiFrameImageStreamCompleter( 120 | codec: key._fetchImageCodec(), 121 | scale: key.scale, 122 | ); 123 | } 124 | 125 | @override 126 | bool operator ==(dynamic other) { 127 | if (other.runtimeType != runtimeType) return false; 128 | final FirebaseImage typedOther = other; 129 | return _imageObject.uri == typedOther._imageObject.uri && 130 | scale == typedOther.scale; 131 | } 132 | 133 | @override 134 | int get hashCode => hashValues(_imageObject.uri, scale); 135 | 136 | @override 137 | String toString() => '$runtimeType("${_imageObject.uri}", scale: $scale)'; 138 | } 139 | -------------------------------------------------------------------------------- /lib/src/image_object.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:firebase_storage/firebase_storage.dart'; 3 | 4 | class FirebaseImageObject { 5 | int version; 6 | final Reference reference; 7 | String? localPath; 8 | final String remotePath; 9 | final String bucket; 10 | final String uri; 11 | 12 | FirebaseImageObject({ 13 | this.version = -1, 14 | required this.reference, 15 | this.localPath, 16 | required this.bucket, 17 | required this.remotePath, 18 | }) : uri = '$bucket$remotePath'; 19 | 20 | Map toMap() { 21 | return { 22 | 'version': version, 23 | 'localPath': localPath, 24 | 'bucket': bucket, 25 | 'remotePath': remotePath, 26 | 'uri': uri, 27 | }; 28 | } 29 | 30 | factory FirebaseImageObject.fromMap(Map map, 31 | [FirebaseApp? firebaseApp]) { 32 | final String bucket = map['bucket']; 33 | final String remotePath = map['remotePath']; 34 | return FirebaseImageObject( 35 | version: map["version"] ?? -1, 36 | reference: _getImageRef(bucket, remotePath, firebaseApp), 37 | localPath: map["localPath"], 38 | bucket: bucket, 39 | remotePath: remotePath, 40 | ); 41 | } 42 | 43 | static Reference _getImageRef( 44 | String bucket, String remotePath, FirebaseApp? firebaseApp) { 45 | FirebaseStorage storage = 46 | FirebaseStorage.instanceFor(app: firebaseApp, bucket: bucket); 47 | return storage.ref().child(remotePath); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: firebase_image 2 | description: A cached Flutter ImageProvider for Firebase Cloud Storage image objects. 3 | version: 1.1.1 4 | homepage: https://github.com/mattreid1/firebase_image 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | firebase_core: ^1.7.0 14 | firebase_storage: ^10.0.5 15 | sqflite: ^2.0.0+4 16 | path: ^1.8.0 17 | path_provider: ^2.0.5 18 | 19 | dev_dependencies: 20 | flutter_lints: ^1.0.4 21 | flutter_test: 22 | sdk: flutter 23 | -------------------------------------------------------------------------------- /test/firebase_image_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | --------------------------------------------------------------------------------