Snackbarを実装したので実装例をご紹介します。
Demo
方針
画面遷移時にSnackbarが急に消えるような体験にしたくなかったため、アプリケーション全体で共通のSnackbarWidgetを表示できるようにします。
また、ViewではなくControllerあるいはViewModel側のエラーハンドリング内で表示ロジックを書くようにしました。そうすることで、関心が分離し、テストが書きやすくなる&実装漏れが防ぎやすくなります。
今回は、riverpodで SnackbarState
状態をもつProviderを作成し、その変更をアプリケーション全体で監視するようにしました。
実装
Provider
Providerは 表示内容を通知する show
メソッドと、stateを初期化する clear
メソッドをもつのみです。
@freezed class SnackbarState with _$SnackbarState { constfactory SnackbarState({ required String message, @Default(SnackbarType.info) SnackbarType type, VoidCallback? onUndo, }) = _SnackbarState; } enum SnackbarType { success, error, info, } @riverpod class Snackbar extends _$Snackbar { @override SnackbarState? build() => null; voidshow(String message, {SnackbarType type = SnackbarType.info, VoidCallback? onUndo}) { state = SnackbarState( message: message, type: type, onUndo: onUndo, ); } void clear() { state = null; } }
View
Snackbarの表示を担う SnackbarListener
Widgetを作成します。
ref.listen(snackbarProvider)
で snackbarProvider
の状態を監視し、現在の状態がnon-null であればSnackbarを表示し、 snackbarProvider
の状態を初期化します。
(scaffoldMessengerKey
については後述します)
class SnackbarListener extends HookConsumerWidget { final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey; final Widget child; const SnackbarListener({ super.key, required this.scaffoldMessengerKey, required this.child, }); @override Widget build(BuildContext context, WidgetRef ref) { ref.listen<SnackbarState?>( snackbarProvider, (previous, current) { if (current == null) return; final undo = current.onUndo; final snackBar = SnackBar( content: Text(current.message), action: current.onUndo != null ? SnackBarAction( label: 'Undo', onPressed: () { if (undo != null) undo(); }, ) : null, ); scaffoldMessengerKey.currentState?.showSnackBar(snackBar); ref.read(snackbarProvider.notifier).clear(); }, ); return child; } }
app.dart
で先程作成した SnackbarListener
を MaterialApp
の上層に追加しました。
class MyApp extends ConsumerWidget { const MyApp({ super.key, }); @override Widget build(BuildContext context, ref) { final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); return AnimatedBuilder( builder: (BuildContext context, Widget? child) => SnackbarListener( scaffoldMessengerKey: scaffoldMessengerKey, child: MaterialApp( scaffoldMessengerKey: scaffoldMessengerKey, ...
ここで、 scaffoldMessengerKey
を作成し、 SnackbarListener
と MaterialApp
それぞれに渡しています。
Snackbar APIは Scaffold 内で呼び出されることを想定されています。(デフォルトでMaterialAppのコンテキストに組み込まれている)
今回は MaterialApp の外側で呼び出しを行ったため、 scaffoldMessengerKey
を渡さなかった場合このようなエラーになってしまいました。
Unhandled Exception: No ScaffoldMessenger widget found. SnackbarListener widgets require a ScaffoldMessenger widget ancestor.
scaffoldMessengerKey
という ScaffoldMessengerState
のグローバルキーを明示的に渡すことで、それぞれのWidgetが同じBuildContextを共有できるようになります。
使用方法
Provider経由でメソッドを呼び出します。Viewを触ることなくSnackbarの表示を実装できました 🎉
final snackbar = ref.read(snackbarProvider.notifier); try { final t = await _taskService.addTask(); snackbar.show('「$title」を追加しました', type: SnackbarType.success); ... } catch (e) { snackbar.show('「$title」の追加に失敗しました', type: SnackbarType.error); ... }