Skip to content

Instantly share code, notes, and snippets.

@stargazing-dino
Created October 22, 2021 19:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stargazing-dino/f0471f01b8f20e276297534dd3564c8a to your computer and use it in GitHub Desktop.
Save stargazing-dino/f0471f01b8f20e276297534dd3564c8a to your computer and use it in GitHub Desktop.
Lets user build an arbitrary image uploader and viewer
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