Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions lib/src/cache_manager/abstract.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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<void> open() async {}

Future<void> close() async {}

Future<FirebaseImageObject?> getObject(
String uri, FirebaseImage image) async {
throw UnimplementedError();
}

Future<List<FirebaseImageObject>> getAllObjects() async {
throw UnimplementedError();
}

Future<Uint8List?> getLocalFileBytes(FirebaseImageObject? object) async {
throw UnimplementedError();
}

Future<Uint8List?> upsertRemoteFileToCache(
FirebaseImageObject object, int maxSizeBytes) async {
throw UnimplementedError();
}

Future<Uint8List?> getRemoteFileBytes(
FirebaseImageObject object, int maxSizeBytes) {
return object.reference.getData(maxSizeBytes);
}

Future<int> getRemoteVersion(
FirebaseImageObject object, int defaultValue) async {
return (await object.reference.getMetadata())
.updated
?.millisecondsSinceEpoch ??
defaultValue;
}

Future<void> 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);
}
}
}
64 changes: 64 additions & 0 deletions lib/src/cache_manager/memory_cache.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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<String, MemoryCacheEntry>();

FirebaseImageCacheManager(cacheRefreshStrategy) : super(cacheRefreshStrategy);

// Interface methods

Future<FirebaseImageObject?> 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<List<FirebaseImageObject>> getAllObjects() async {
final List<FirebaseImageObject> objects = [];
memoryCache.forEach((k, v) => objects.add(v.object));
return objects;
}

Future<Uint8List?> getLocalFileBytes(FirebaseImageObject? object) async {
final cacheEntry = memoryCache[object?.uri];
return cacheEntry?.bytes;
}

Future<Uint8List?> 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;
}
}
Loading