Flutter: go_router で共通の親ウィジェット・処理を持たせたい

Flutter: go_router で共通の親ウィジェット・処理を持たせたい

December 7, 2022

次の要件があるとする。

「ユーザーのネットワーク接続状態を監視しておき、インターネット未接続状態であればスナックバーでそのことを通知する。」

まず単純に共通の親ウィジェット・処理を持たせるのであれば、以下のように MaterialApp.router() を囲むウィジェットを定義すれば実現できる。

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MyRootWidget(
      child: MaterialApp.router(
        title: 'Example App',
        routerDelegate: router.routerDelegate,
        routeInformationProvider: router.routeInformationProvider,
        routeInformationParser: router.routeInformationParser,
      ),
    );
  }
}
class MyRootWidget extends StatefulWidget {
  const MyRootWidget({required this.child, super.key});

  @override
  State<MyRootWidget> createState() => _MyRootWidgetState();

  final Widget child;
}

class _MyRootWidgetState extends State<MyRootWidget> {
  @override
  void initState() {
    super.initState();
    //
    // 必要な処理を実施
    //
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

今回はスナックバーを表示させたいのだが、いまの作りだと MyRootWidget は MaterialApp の外側に配置しているため ScaffoldMessenger.of(context).showSnackBar(); するとエラーになってしまう。

対応方法1:go_router の ShellRoute を利用する #

すべてのページの ShellRoute として共通のウィジェットを設定する。こうすれば MaterialApp の内側に配置されるため ScaffoldMessenger.of(context) はエラーにならない。

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Example App',
      routerDelegate: router.routerDelegate,
      routeInformationProvider: router.routeInformationProvider,
      routeInformationParser: router.routeInformationParser,
    );
  }
}
final GoRouter router = GoRouter(
  initialLocation: '/home',
  routes: <RouteBase>[
    // 👇 ShellRoute
    ShellRoute(
      builder: (BuildContext context, GoRouterState state, Widget child) {
        return ShellRouteWidget(child: child);
      },
      routes: <RouteBase>[
        GoRoute(
          path: '/home',
          pageBuilder: (_, GoRouterState state) => const NoTransitionPage<void>(
            child: HomePage(),
          ),
        ),
        GoRoute(
          path: '/setting',
          pageBuilder: (_, GoRouterState state) => const NoTransitionPage<void>(
            child: SettingPage(),
          ),
        ),
      ],
    ),
  ],
);
class ShellRouteWidget extends StatefulWidget {
  const ShellRouteWidget({required this.child, super.key});

  @override
  State<ShellRouteWidget> createState() => _ShellRouteWidgetState();

  final Widget child;
}

class _ShellRouteWidgetState extends State<ShellRouteWidget> {
  @override
  void initState() {
    super.initState();
    //
    // 必要な処理を実施
    //
    // スナックバーを開くには、
    ScaffoldMessenger.of(context).showSnackBar(...);
    //
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

対応方法2:MaterialApp の scaffoldMessengerKey を利用する #

グローバルな scaffoldMessengerKey を生成して MaterialApp に設定する。こうすれば MaterialApp 配下のウィジェットでなくても MaterialApp にアクセス可能となる。

final GlobalKey<ScaffoldMessengerState> scaffoldKey = GlobalKey<ScaffoldMessengerState>();
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MyRootWidget(
      child: MaterialApp.router(
        // 👇 scaffoldMessengerKey
        scaffoldMessengerKey: scaffoldKey,
        title: 'Example App',
        routerDelegate: router.routerDelegate,
        routeInformationProvider: router.routeInformationProvider,
        routeInformationParser: router.routeInformationParser,
      ),
    );
  }
}
class MyRootWidget extends StatefulWidget {
  const MyRootWidget({required this.child, super.key});

  @override
  State<MyRootWidget> createState() => _MyRootWidgetState();

  final Widget child;
}

class _MyRootWidgetState extends State<MyRootWidget> {
  @override
  void initState() {
    super.initState();
    //
    // 必要な処理を実施
    //
    // スナックバーを開くには、
    final ScaffoldMessengerState? scaffoldMessangerState = scaffoldKey.currentState;
    scaffoldMessangerState?.showSnackBar(...);
    //
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}