Quantcast
Channel: プログラミング
Viewing all articles
Browse latest Browse all 7811

flutter: Snackbarを全体管理する - kokh log

$
0
0

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の表示を担う SnackbarListenerWidgetを作成します。   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で先程作成した SnackbarListenerMaterialAppの上層に追加しました。

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を作成し、 SnackbarListenerMaterialAppそれぞれに渡しています。
Snackbar APIは Scaffold 内で呼び出されることを想定されています。(デフォルトでMaterialAppのコンテキストに組み込まれている)
今回は MaterialApp の外側で呼び出しを行ったため、 scaffoldMessengerKeyを渡さなかった場合このようなエラーになってしまいました。

Unhandled Exception: No ScaffoldMessenger widget found.
SnackbarListener widgets require a ScaffoldMessenger widget ancestor.

scaffoldMessengerKeyという ScaffoldMessengerStateのグローバルキーを明示的に渡すことで、それぞれのWidgetが同じBuildContextを共有できるようになります。

docs.flutter.dev

使用方法

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);
  ...
}

Viewing all articles
Browse latest Browse all 7811

Trending Articles