Created
October 22, 2021 19:13
-
-
Save stargazing-dino/f0471f01b8f20e276297534dd3564c8a to your computer and use it in GitHub Desktop.
Lets user build an arbitrary image uploader and viewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:cached_network_image/cached_network_image.dart'; | |
import 'package:firebase_storage/firebase_storage.dart'; | |
import 'package:firebase_storage_hooks/firebase_storage_hooks.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_cache_manager_firebase/flutter_cache_manager_firebase.dart'; | |
import 'package:flutter_hooks/flutter_hooks.dart'; | |
export 'package:firebase_storage_hooks/firebase_storage_hooks.dart'; | |
/// Opens a file picker and selects an image. If the image is not null, | |
/// it will be uploaded to firebase storage. If a previous image existed | |
/// this function will remove that too upon upload of the replacement image | |
typedef UploadHandler = Widget Function(ImageUploaderHandler); | |
class ImageUploaderHandler { | |
const ImageUploaderHandler({ | |
required this.getImage, | |
required this.commit, | |
required this.commitFunction, | |
required this.fileSnapshot, | |
required this.storageSnapshot, | |
required this.imageProvider, | |
required this.storageWidget, | |
}); | |
final Future<PlatformFile?> Function() getImage; | |
final Future<CommitHandler<String>?> Function([PlatformFile? platformFile]) | |
commit; | |
final Future<CommitHandler<String>?> Function()? Function() commitFunction; | |
final ImageProvider? imageProvider; | |
final Widget storageWidget; | |
final AsyncSnapshot<PlatformFile> fileSnapshot; | |
final ValueListenable<AsyncSnapshotWithProgress<String, TaskSnapshot>> | |
storageSnapshot; | |
} | |
/// A firebase image has a few states: | |
/// | |
/// 1. No network image and no selected image | |
/// 2. A network image and no selected image | |
/// | |
/// 3. No network image and a selected image | |
/// 4. A network image and a selected image (show dialog saying are you sure you want to replace the image) | |
/// | |
/// Not only that but we have two async both with snapshots states... | |
/// 1. nothing | |
/// 2. loading | |
/// 3. data | |
/// 4. error | |
/// | |
/// Lifetime of an image: | |
/// | |
/// Image: Image + Gesture detector to detect taps and select an image | |
/// No image: IconButton to select an image | |
/// Loading: ProgressIndicator | |
/// Error: Error IconButton button to retry selection | |
/// Data: Memory.Image or File.Image | |
/// | |
/// user hits commit() -> All of these are have an image now and wrap the image | |
/// in some way | |
/// Loading: CircularProgressIndicator around the image | |
/// Error: Error IconButton button above image to rety commit | |
/// Data: nothing as image is showing | |
/// | |
/// There we'll break the process into two builders. One is passed the image | |
class FirebaseImageBuilder extends HookWidget { | |
const FirebaseImageBuilder({ | |
Key? key, | |
required this.imageUrl, | |
required this.location, | |
this.imageSelectedBuilder, | |
this.imageNothingBuilder, | |
this.imageErrorBuilder, | |
this.imageLoadingBuilder, | |
this.showUploadSuccessSnackbar = true, | |
this.placeholder, | |
this.storageNothing, | |
this.storageErrorBuilder, | |
this.storageUploadingBuilder, | |
this.storageUploadedBuilder, | |
required this.builder, | |
}) : super(key: key); | |
final String? imageUrl; | |
final String location; | |
/// The user has selected an image | |
final ImageProvider? Function(PlatformFile)? imageSelectedBuilder; | |
/// The user has yet to select an image. However, if they have a valid | |
/// [imageUrl] then that will be shown through a [CachedNetworkImageProvider] | |
/// instead. | |
final ImageProvider? Function(String? previousImageUrl)? imageNothingBuilder; | |
/// An error occurred while selecting the image. No image provider is returned. | |
final ImageProvider? Function( | |
Object, | |
StackTrace?, | |
PlatformFile?, | |
)? imageErrorBuilder; | |
/// The selected image is loading. No image provider is returned. | |
final ImageProvider? Function(PlatformFile?)? imageLoadingBuilder; | |
/// A widget shown when both the storage snapshot and the file snapshot are in | |
/// nothing state. | |
final Widget? placeholder; | |
final Widget? storageNothing; | |
final bool showUploadSuccessSnackbar; | |
final Widget Function(Object, StackTrace?, String?)? storageErrorBuilder; | |
final Widget Function(TaskSnapshot, String?)? storageUploadingBuilder; | |
final Widget Function(String)? storageUploadedBuilder; | |
final UploadHandler builder; | |
@override | |
Widget build(BuildContext context) { | |
final Reference? oldReference = FirebaseStorage.instance.ref(imageUrl); | |
final reference = FirebaseStorage.instance.ref(location); | |
final theme = Theme.of(context); | |
final storageFileHandler = useStorageFile( | |
settableMetadata: SettableMetadata(contentType: 'image/jpeg'), | |
fileType: FileType.image, | |
reference: reference, | |
fileNameBuilder: (fileName) { | |
final uniqueKey = UniqueKey(); | |
final storageFileName = | |
'${shortHash(uniqueKey)}-$fileName'.replaceAll(RegExp(r'\s+'), '_'); | |
return storageFileName; | |
}, | |
); | |
Future<PlatformFile?> getImage() async { | |
final platformFile = await storageFileHandler.fileHandler.select(); | |
if (platformFile == null) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text('No image selected'), | |
), | |
); | |
return null; | |
} | |
return platformFile; | |
} | |
Future<CommitHandler<String>?> commit([PlatformFile? platformFile]) async { | |
if (imageUrl != null) { | |
final shouldCommit = await showDialog<bool>( | |
context: context, | |
builder: (context) { | |
return AlertDialog( | |
title: const Text('Are you sure you want to replace the image?'), | |
actions: [ | |
SimpleDialogOption( | |
child: const Text('Yes'), | |
onPressed: () { | |
Navigator.pop(context, true); | |
}, | |
), | |
SimpleDialogOption( | |
child: const Text('No'), | |
onPressed: () { | |
Navigator.pop(context, false); | |
}, | |
), | |
], | |
); | |
}, | |
); | |
if (shouldCommit == null || shouldCommit == false) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar(content: Text('Upload cancelled')), | |
); | |
return null; | |
} | |
} | |
try { | |
await oldReference?.delete(); | |
final commitHandler = | |
await storageFileHandler.maybeCommit(platformFile); | |
if (commitHandler != null && showUploadSuccessSnackbar) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar(content: Text('Uploaded image')), | |
); | |
} | |
return commitHandler; | |
} catch (error) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text('Image upload failed'), | |
), | |
); | |
rethrow; | |
} | |
} | |
Future<CommitHandler<String>?> Function()? commitFunction() { | |
return storageFileHandler.fileHandler.snapshot.maybeWhen( | |
data: (platformFile) => () => commit(platformFile), | |
orElse: () => null, | |
); | |
} | |
final fileSnapshot = storageFileHandler.fileHandler.snapshot; | |
final imageProvider = fileSnapshot.when( | |
data: (file) { | |
final _imageSelectedBuilder = imageSelectedBuilder; | |
if (_imageSelectedBuilder != null) { | |
return _imageSelectedBuilder(file); | |
} else { | |
if (kIsWeb) { | |
return MemoryImage(file.bytes!); | |
} else { | |
return AssetImage(file.path!); | |
} | |
} | |
}, | |
nothing: () { | |
final _imageNothingBuilder = imageNothingBuilder; | |
if (_imageNothingBuilder != null) { | |
return _imageNothingBuilder(imageUrl); | |
} else { | |
final _imageUrl = imageUrl; | |
if (_imageUrl == null) { | |
return null; | |
} | |
return CachedNetworkImageProvider( | |
_imageUrl, | |
cacheManager: FirebaseCacheManager(), | |
); | |
} | |
}, | |
error: (Object error, StackTrace? stackTrace, PlatformFile? previous) { | |
final _imageErrorBuilder = imageErrorBuilder; | |
if (_imageErrorBuilder != null) { | |
return _imageErrorBuilder(error, stackTrace, previous); | |
} else { | |
return null; | |
} | |
}, | |
loading: (PlatformFile? previous) { | |
final _imageLoadingBuilder = imageLoadingBuilder; | |
if (_imageLoadingBuilder != null) { | |
return _imageLoadingBuilder(previous); | |
} else { | |
return null; | |
} | |
}, | |
); | |
final storageWidget = | |
ValueListenableBuilder<AsyncSnapshotWithProgress<String, TaskSnapshot>>( | |
valueListenable: storageFileHandler.snapshotListenable, | |
builder: (context, snapshot, child) { | |
return snapshot.when( | |
error: (error, stackTrace, previous) { | |
final _storageErrorBuilder = storageErrorBuilder; | |
if (_storageErrorBuilder != null) { | |
return _storageErrorBuilder(error, stackTrace, previous); | |
} else { | |
return Icon(Icons.error, color: theme.errorColor); | |
} | |
}, | |
loading: (task, previous) { | |
final _storageUploadingBuilder = storageUploadingBuilder; | |
if (_storageUploadingBuilder != null) { | |
return _storageUploadingBuilder(task, previous); | |
} else { | |
final _progress = task.bytesTransferred / task.totalBytes; | |
return CircularProgressIndicator(value: _progress); | |
} | |
}, | |
data: (remoteUrl) { | |
final _storageUploadedBuilder = storageUploadedBuilder; | |
if (_storageUploadedBuilder != null) { | |
return _storageUploadedBuilder(remoteUrl); | |
} else { | |
return const SizedBox.shrink(); | |
} | |
}, | |
nothing: () { | |
final _storageNothing = storageNothing; | |
if (_storageNothing != null) { | |
return _storageNothing; | |
} else { | |
if (imageUrl != null) { | |
return const SizedBox.shrink(); | |
} | |
return fileSnapshot.maybeWhen( | |
data: (file) => const SizedBox.shrink(), | |
orElse: () { | |
return placeholder ?? const Icon(Icons.cloud_upload); | |
}, | |
); | |
} | |
}, | |
); | |
}, | |
); | |
return builder( | |
ImageUploaderHandler( | |
getImage: getImage, | |
commit: commit, | |
commitFunction: commitFunction, | |
fileSnapshot: fileSnapshot, | |
storageSnapshot: storageFileHandler.snapshotListenable, | |
imageProvider: imageProvider, | |
storageWidget: storageWidget, | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment