🍣

Flutter go_router_builderふんわり解説

2023/12/01に公開

go_router_builderとは

ざっくりいうとgo_routerbuild_runnerを使った自動生成を使って

より使いやすくできるようにするものがgo_router_builderです


何がどう使いやすくなるのか

  1. 自動生成によりコード記述量を減らせる
  2. 自動生成によりコードの統一化をしやすく、属人性を抑えることができる
  3. 引数付き画面遷移を型安全に行うことができるようになる

といった感じになります


何かデメリットはないの??というところが気になると思いますが

強いてあげるのであれば自動生成がめんどくさいという点ですが

それでも個人的には型安全に画面遷移できるメリットの方が大きいと感じているので

go_router_builder を使った方がいいと思ってます

導入方法

公式ドキュメントを見た方が早いです

https://pub.dev/packages/go_router_builder/example

なのでここではgo_routerとどう変わるかだけ説明します


go_router
final GoRouter _router = GoRouter(
  routes: <RouteBase>[
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) {
        return const HomeScreen();
      },
      routes: <RouteBase>[
        GoRoute(
          path: 'details',
          builder: (BuildContext context, GoRouterState state) {
            return const DetailsScreen();
          },
        ),
      ],
    ),
  ],
);

/// The main app.
class MyApp extends StatelessWidget {
  /// Constructs a [MyApp]
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}


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

  final LoginInfo loginInfo = LoginInfo();
  static const String title = 'GoRouter Example: Named Routes';

  
  Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
        value: loginInfo,
        child: MaterialApp.router(
          routerConfig: _router,
          title: title,
          debugShowCheckedModeBanner: false,
        ),
      );

  late final GoRouter _router = GoRouter(
    debugLogDiagnostics: true,
    routes: $appRoutes,

    // redirect to the login page if the user is not logged in
    redirect: (BuildContext context, GoRouterState state) {
      final bool loggedIn = loginInfo.loggedIn;

      // check just the matchedLocation in case there are query parameters
      final String loginLoc = const LoginRoute().location;
      final bool goingToLogin = state.matchedLocation == loginLoc;

      // the user is not logged in and not headed to /login, they need to login
      if (!loggedIn && !goingToLogin) {
        return LoginRoute(fromPage: state.matchedLocation).location;
      }

      // the user is logged in and headed to /login, no need to login again
      if (loggedIn && goingToLogin) {
        return const HomeRoute().location;
      }

      // no need to redirect at all
      return null;
    },

    // changes on the listenable will cause the router to refresh it's route
    refreshListenable: loginInfo,
  );
}


こちらのコードはgo_routergo_router_builder

exampleコードをそのまま持ってきたものです

https://pub.dev/packages/go_router/example

https://pub.dev/packages/go_router_builder/example


go_router_builderは大元がいきなりChangeNotifierProviderになってますが

そこはあまり気にしなくて大丈夫です

要は両者ともにMaterialApp.routerrouterConfigにrouterを渡すことで

設定ができているということがわかれば十分だと思います

なので両者ともに導入時点では特に違いはありません


両者で大きく違う点はGoRouter定義のここの部分です

go_router
final GoRouter _router = GoRouter(
  routes: <RouteBase>[
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) {
        return const HomeScreen();
      },
      routes: <RouteBase>[
        GoRoute(
          path: 'details',
          builder: (BuildContext context, GoRouterState state) {
            return const DetailsScreen();
          },
        ),
      ],
    ),
  ],
);


go_router_builer
late final GoRouter _router = GoRouter(
    debugLogDiagnostics: true,
    routes: $appRoutes,


go_routerは直接Routeを書いて渡しているのに対して

go_router_builderがよくわからない$appRoutes変数を渡しているだけとなっています


では$appRoutesが何かというと、

これがbuild_runnerにより自動生成で作られたルーティングです


ルーティング(画面遷移基盤)の作り方

go_router_builder_example
<HomeRoute>(
  path: '/',
  routes: <TypedGoRoute<GoRouteData>>[
    TypedGoRoute<FamilyRoute>(
      path: 'family/:fid',
      routes: <TypedGoRoute<GoRouteData>>[
        TypedGoRoute<PersonRoute>(
          path: 'person/:pid',
          routes: <TypedGoRoute<GoRouteData>>[
            TypedGoRoute<PersonDetailsRoute>(path: 'details/:details'),
          ],
        ),
      ],
    ),
    TypedGoRoute<FamilyCountRoute>(path: 'family-count/:count'),
  ],
)
class HomeRoute extends GoRouteData {
  const HomeRoute();

  
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

<LoginRoute>(
  path: '/login',
)
class LoginRoute extends GoRouteData {
  const LoginRoute({this.fromPage});

  final String? fromPage;

  
  Widget build(BuildContext context, GoRouterState state) =>
      LoginScreen(from: fromPage);
}

class FamilyRoute extends GoRouteData {
  const FamilyRoute(this.fid);

  final String fid;

  
  Widget build(BuildContext context, GoRouterState state) =>
      FamilyScreen(family: familyById(fid));
}

class PersonRoute extends GoRouteData {
  const PersonRoute(this.fid, this.pid);

  final String fid;
  final int pid;

  
  Widget build(BuildContext context, GoRouterState state) {
    final Family family = familyById(fid);
    final Person person = family.person(pid);
    return PersonScreen(family: family, person: person);
  }
}

class PersonDetailsRoute extends GoRouteData {
  const PersonDetailsRoute(this.fid, this.pid, this.details, {this.$extra});

  final String fid;
  final int pid;
  final PersonDetails details;
  final int? $extra;

  
  Page<void> buildPage(BuildContext context, GoRouterState state) {
    final Family family = familyById(fid);
    final Person person = family.person(pid);

    return MaterialPage<Object>(
      fullscreenDialog: true,
      key: state.pageKey,
      child: PersonDetailsPage(
        family: family,
        person: person,
        detailsKey: details,
        extra: $extra,
      ),
    );
  }
}

class FamilyCountRoute extends GoRouteData {
  const FamilyCountRoute(this.count);

  final int count;

  
  Widget build(BuildContext context, GoRouterState state) => FamilyCountScreen(
        count: count,
      );
}

こちらもgo_router_builderのexampleから抜粋したコードです

ルーティングの基本は

<HogeRoute>

という形で宣言することで作ることができます


exampleの方はどうなっているのかというと

<HomeRoute>(
  path: '/',

という形で宣言されており、

パスにpath: '/'が指定されているのでイニシャルルートということがわかります


では画面はどこにあるのかというと@TypedGoRoute<HomeRoute>で型宣言されている

HomeRouteが画面側に本体になります

home_route
class HomeRoute extends GoRouteData {
  const HomeRoute();

  
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

HomeRouteを見ると非常にわかりやすく、

GoRouteDataを継承し、Widget buildをoverrideして

HomeScreenを返しているという形です


ではここから画面遷移はどのように定義するのかというと、

<HomeRoute>(
  path: '/',
  routes: <TypedGoRoute<GoRouteData>>[
    TypedGoRoute<FamilyRoute>(
      path: 'family/:fid',
      routes: <TypedGoRoute<GoRouteData>>[
        TypedGoRoute<PersonRoute>(
          path: 'person/:pid',
          routes: <TypedGoRoute<GoRouteData>>[
            TypedGoRoute<PersonDetailsRoute>(path: 'details/:details'),
          ],
        ),
      ],
    ),
    TypedGoRoute<FamilyCountRoute>(path: 'family-count/:count'),
  ],
)

こちらのroutesの中でルーティング宣言をすることで画面遷移を構築することができます

ネストしたルーティングを作りたい場合は

routes: <TypedGoRoute<GoRouteData>>[
    TypedGoRoute<FamilyRoute>(
      path: 'family/:fid',
      routes: <TypedGoRoute<GoRouteData>>[
        TypedGoRoute<PersonRoute>(
          path: 'person/:pid',
          routes: <TypedGoRoute<GoRouteData>>[
            TypedGoRoute<PersonDetailsRoute>(path: 'details/:details'),
          ],
        ),
      ],
    ),

TypedGoRoute宣言したroutesの中で更にTypedGoRoute宣言することで

作ることができるということですね

ここまで理解できると上記のexampleコードが非常に見やすくなったと思います

  • イニシャルルートがHomeRoute
  • HomeRouteはFamilyRouteとFamilyCountRouteの画面遷移を並列に持っている
  • FamilyRouteはPersonRoute->PersonDetailsRouteといったネストされたルーティングを持っている

という感じですね


画面遷移方法

まずはexampleのコードを見てみましょう

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

  
  Widget build(BuildContext context) {
    final LoginInfo info = context.read<LoginInfo>();

    return Scaffold(
      appBar: AppBar(
        title: const Text(App.title),
        centerTitle: true,
        actions: <Widget>[
          PopupMenuButton<String>(
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<String>>[
                PopupMenuItem<String>(
                  value: '1',
                  child: const Text('Push w/o return value'),
                  onTap: () => const PersonRoute('f1', 1).push(context),
                ),
                PopupMenuItem<String>(
                  value: '2',
                  child: const Text('Push w/ return value'),
                  onTap: () async {
                    unawaited(FamilyCountRoute(familyData.length)
                        .push<int>(context)
                        .then((int? value) {
                      if (value != null) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text('Age was: $value'),
                          ),
                        );
                      }
                    }));
                  },
                ),
                PopupMenuItem<String>(
                  value: '3',
                  child: Text('Logout: ${info.userName}'),
                  onTap: () => info.logout(),
                ),
              ];
            },
          ),
        ],
      ),
      body: ListView(
        children: <Widget>[
          for (final Family f in familyData)
            ListTile(
              title: Text(f.name),
              onTap: () => FamilyRoute(f.id).go(context),
            )
        ],
      ),
    );
  }
}
	
class PersonRoute extends GoRouteData {
  const PersonRoute(this.fid, this.pid);

  final String fid;
  final int pid;

  
  Widget build(BuildContext context, GoRouterState state) {
    final Family family = familyById(fid);
    final Person person = family.person(pid);
    return PersonScreen(family: family, person: person);
  }
}


これのどこで画面遷移をしているんだというとここです

onTap: () => const PersonRoute('f1', 1).push(context),
	
onTap: () => FamilyRoute(f.id).go(context),

画面遷移方法は作ったルートに対してpushまたはgoすることで

画面遷移ができるということですね


goとpush何が違うの?

何が違うの?というお題にしたものの、

これはgo_router_builderの話というよりgo_router側の話に近いです

理由としてはgo_router_builderで作ったルートからgoやpushするイベントは

router.g.dartで自動生成され、

router.g.dartで使っているgoやpushは

pub.dev/go_router-10.1.1/lib/src/misc/extensions.dart

こちらを参照しているので大元はgo_routerのイベントということです


一応おさらいとして何がどう違うかというと、

/// Dart extension to add navigation function to a BuildContext object, e.g.
/// context.go('/');
extension GoRouterHelper on BuildContext {
  /// Get a location from route name and parameters.
  ///
  /// This method can't be called during redirects.
  String namedLocation(
    String name, {
    Map<String, String> pathParameters = const <String, String>{},
    Map<String, dynamic> queryParameters = const <String, dynamic>{},
  }) =>
      GoRouter.of(this).namedLocation(name,
          pathParameters: pathParameters, queryParameters: queryParameters);

  /// Navigate to a location.
  void go(String location, {Object? extra}) =>
      GoRouter.of(this).go(location, extra: extra);

  /// Navigate to a named route.
  void goNamed(
    String name, {
    Map<String, String> pathParameters = const <String, String>{},
    Map<String, dynamic> queryParameters = const <String, dynamic>{},
    Object? extra,
  }) =>
      GoRouter.of(this).goNamed(
        name,
        pathParameters: pathParameters,
        queryParameters: queryParameters,
        extra: extra,
      );

  /// Push a location onto the page stack.
  ///
  /// See also:
  /// * [pushReplacement] which replaces the top-most page of the page stack and
  ///   always uses a new page key.
  /// * [replace] which replaces the top-most page of the page stack but treats
  ///   it as the same page. The page key will be reused. This will preserve the
  ///   state and not run any page animation.
  Future<T?> push<T extends Object?>(String location, {Object? extra}) =>
      GoRouter.of(this).push<T>(location, extra: extra);

  /// Navigate to a named route onto the page stack.
  Future<T?> pushNamed<T extends Object?>(
    String name, {
    Map<String, String> pathParameters = const <String, String>{},
    Map<String, dynamic> queryParameters = const <String, dynamic>{},
    Object? extra,
  }) =>
      GoRouter.of(this).pushNamed<T>(
        name,
        pathParameters: pathParameters,
        queryParameters: queryParameters,
        extra: extra,
      );

  /// Returns `true` if there is more than 1 page on the stack.
  bool canPop() => GoRouter.of(this).canPop();

  /// Pop the top page off the Navigator's page stack by calling
  /// [Navigator.pop].
  void pop<T extends Object?>([T? result]) => GoRouter.of(this).pop(result);

  /// Replaces the top-most page of the page stack with the given URL location
  /// w/ optional query parameters, e.g. `/family/f2/person/p1?color=blue`.
  ///
  /// See also:
  /// * [go] which navigates to the location.
  /// * [push] which pushes the given location onto the page stack.
  /// * [replace] which replaces the top-most page of the page stack but treats
  ///   it as the same page. The page key will be reused. This will preserve the
  ///   state and not run any page animation.
  void pushReplacement(String location, {Object? extra}) =>
      GoRouter.of(this).pushReplacement(location, extra: extra);

  /// Replaces the top-most page of the page stack with the named route w/
  /// optional parameters, e.g. `name='person', pathParameters={'fid': 'f2', 'pid':
  /// 'p1'}`.
  ///
  /// See also:
  /// * [goNamed] which navigates a named route.
  /// * [pushNamed] which pushes a named route onto the page stack.
  void pushReplacementNamed(
    String name, {
    Map<String, String> pathParameters = const <String, String>{},
    Map<String, dynamic> queryParameters = const <String, dynamic>{},
    Object? extra,
  }) =>
      GoRouter.of(this).pushReplacementNamed(
        name,
        pathParameters: pathParameters,
        queryParameters: queryParameters,
        extra: extra,
      );

  /// Replaces the top-most page of the page stack with the given one but treats
  /// it as the same page.
  ///
  /// The page key will be reused. This will preserve the state and not run any
  /// page animation.
  ///
  /// See also:
  /// * [push] which pushes the given location onto the page stack.
  /// * [pushReplacement] which replaces the top-most page of the page stack but
  ///   always uses a new page key.
  void replace(String location, {Object? extra}) =>
      GoRouter.of(this).replace(location, extra: extra);

  /// Replaces the top-most page with the named route and optional parameters,
  /// preserving the page key.
  ///
  /// This will preserve the state and not run any page animation. Optional
  /// parameters can be provided to the named route, e.g. `name='person',
  /// pathParameters={'fid': 'f2', 'pid': 'p1'}`.
  ///
  /// See also:
  /// * [pushNamed] which pushes the given location onto the page stack.
  /// * [pushReplacementNamed] which replaces the top-most page of the page
  ///   stack but always uses a new page key.
  void replaceNamed(
    String name, {
    Map<String, String> pathParameters = const <String, String>{},
    Map<String, dynamic> queryParameters = const <String, dynamic>{},
    Object? extra,
  }) =>
      GoRouter.of(this).replaceNamed(name,
          pathParameters: pathParameters,
          queryParameters: queryParameters,
          extra: extra);
}


こちらはextensions.dartのまるまる引用です

今回はgo,push,replaceの翻訳だけ載せます


原文
/// * [go] which navigates to the location.
  /// * [push] which pushes the given location onto the page stack.
  /// * [replace] which replaces the top-most page of the page stack but treats
  ///   it as the same page. The page key will be reused. This will preserve the
  ///   state and not run any page animation.
翻訳
/// * [go] は、その場所に移動します。
  /// * [push] は、指定された場所をページ スタックにプッシュします。
  /// * [replace] ページ スタックの最上位ページを置換しますが、
  /// 同じページとして扱われます。ページキーは再利用されます。これにより、
  /// 状態を維持し、ページ アニメーションを実行しません。

ざっくりいうと

  • goは指定したルートに移動
  • pushは指定したルートを強制的に最上位にスタックさせる
  • replaceはスタックの最上位画面を置換させる(同じ画面として扱うので遷移アニメーションも発生しない)

という感じですね


goとpushの更に詳しい話

解説についてはアンドレアさんがいい記事を書いているので紹介します

https://codewithandrea.com/articles/flutter-navigation-gorouter-go-vs-push/

ざっくり説明すると

  • pushは強制的に最上位にスタックさせるのでDeepLinkが壊れる恐れがある
  • 基本的にgoで統一した方がDeepLinkが壊れないのでいいよ

ということと解釈してます

この辺は実際にバグを踏んだ事がないので正直よくわかってないです

今後経験したらまた記事を更新するかもしれません

(go_router自信ニキがいたらコメントで教えてください🙇)


値渡し関連

// 画面遷移アクション
onTap: () => const PersonRoute('f1', 1).push(context),

// ルート定義
class PersonRoute extends GoRouteData {
  const PersonRoute(this.fid, this.pid);

  final String fid;
  final int pid;

  
  Widget build(BuildContext context, GoRouterState state) {
    final Family family = familyById(fid);
    final Person person = family.person(pid);
    return PersonScreen(family: family, person: person);
  }
}
	
// ルート基盤
TypedGoRoute<PersonRoute>(
          path: 'person/:pid',
          routes: <TypedGoRoute<GoRouteData>>[
            TypedGoRoute<PersonDetailsRoute>(path: 'details/:details'),
          ],
        ),

こちらはexampleコードから抜粋したものです

画面遷移する際にPersonRoute('f1', 1).push(context)となっており、

PersonRoute側で引数の設定

TypedGoRoute<PersonRoute>のパスで

path: 'person/:pid',

という形で:hogeとしてクエリパラメータの設定がされています

PersonRouteで明確に型定義がされているのでタイプセーフに画面遷移ができるという事ですね


値渡し関連の詳しい解説はすささんの記事がわかりやすいのでおすすめです

https://zenn.dev/flutteruniv_dev/articles/20220801-135028-flutter-go-router-builder


ボトムナビゲーションの作り方

ボトムナビゲーションについてはpuv.devのexampleにはないものの

公式リポジトリのサンプルコードの中にサンプルがあるのでそちらを引用します

https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/shell_route_example.dart


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

  
  Widget build(BuildContext context) => MaterialApp.router(
        routerConfig: _router,
      );

  final GoRouter _router = GoRouter(
    routes: $appRoutes,
    initialLocation: '/foo',
  );
}

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

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('foo')),
      );
}

<MyShellRouteData>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<FooRouteData>(path: '/foo'),
    TypedGoRoute<BarRouteData>(path: '/bar'),
  ],
)
class MyShellRouteData extends ShellRouteData {
  const MyShellRouteData();

  
  Widget builder(
    BuildContext context,
    GoRouterState state,
    Widget navigator,
  ) {
    return MyShellRouteScreen(child: navigator);
  }
}

class FooRouteData extends GoRouteData {
  const FooRouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const FooScreen();
  }
}

class BarRouteData extends GoRouteData {
  const BarRouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const BarScreen();
  }
}

class MyShellRouteScreen extends StatelessWidget {
  const MyShellRouteScreen({required this.child, super.key});

  final Widget child;

  int getCurrentIndex(BuildContext context) {
    final String location = GoRouterState.of(context).uri.toString();
    if (location == '/bar') {
      return 1;
    }
    return 0;
  }

  
  Widget build(BuildContext context) {
    final int currentIndex = getCurrentIndex(context);
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Foo',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Bar',
          ),
        ],
        onTap: (int index) {
          switch (index) {
            case 0:
              const FooRouteData().go(context);
              break;
            case 1:
              const BarRouteData().go(context);
              break;
          }
        },
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return const Text('Foo');
  }
}

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

  
  Widget build(BuildContext context) {
    return const Text('Bar');
  }
}

<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData {
  const LoginRoute();

  
  Widget build(BuildContext context, GoRouterState state) =>
      const LoginScreen();
}

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

  
  Widget build(BuildContext context) {
    return const Text('Login');
  }
}


routerの設定は同じなのでどこが変わるかというとルート基盤の構築方法が変わります

<MyShellRouteData>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<FooRouteData>(path: '/foo'),
    TypedGoRoute<BarRouteData>(path: '/bar'),
  ],
)
class MyShellRouteData extends ShellRouteData {
  const MyShellRouteData();

  
  Widget builder(
    BuildContext context,
    GoRouterState state,
    Widget navigator,
  ) {
    return MyShellRouteScreen(child: navigator);
  }
}

class FooRouteData extends GoRouteData {
  const FooRouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const FooScreen();
  }
}

class BarRouteData extends GoRouteData {
  const BarRouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const BarScreen();
  }
}


@TypedGoRoute<HogeRoute>していたものが

@TypedShellRoute<HogeRouteData>という形に変わってます

そしてTypedShellRouteがroutesとして

routes: <TypedRoute<RouteData>>[
    TypedGoRoute<FooRouteData>(path: '/foo'),
    TypedGoRoute<BarRouteData>(path: '/bar'),
  ],

を持っているという形になっていますね

一番大きく違う点は親のHogeRouteDataがパスを持っていません

そしてイニシャルルートとして設定されているものは

final GoRouter _router = GoRouter(
    routes: $appRoutes,
    initialLocation: '/foo',
  );

となっており、

子のFooRouteDataがイニシャルルートになっています


どういうことかというと親のHogeRouteDataはただの箱になっていて、

ルートとしては子が本体になっているという形になっています

画面本体を見ると非常にわかりやすいです


class MyShellRouteData extends ShellRouteData {
  const MyShellRouteData();

  
  Widget builder(
    BuildContext context,
    GoRouterState state,
    Widget navigator,
  ) {
    return MyShellRouteScreen(child: navigator);
  }
}
	
class MyShellRouteScreen extends StatelessWidget {
  const MyShellRouteScreen({required this.child, super.key});

  final Widget child;

  int getCurrentIndex(BuildContext context) {
    final String location = GoRouterState.of(context).uri.toString();
    if (location == '/bar') {
      return 1;
    }
    return 0;
  }

  
  Widget build(BuildContext context) {
    final int currentIndex = getCurrentIndex(context);
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Foo',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Bar',
          ),
        ],
        onTap: (int index) {
          switch (index) {
            case 0:
              const FooRouteData().go(context);
              break;
            case 1:
              const BarRouteData().go(context);
              break;
          }
        },
      ),
    );
  }
}

MyShellRouteData extends ShellRouteDataは

Widgetとしてnavigatorを返しており

navigatorをMyShellRouteScreenが受け取っています


MyShellRouteScreen側は受け取ったnavigatorをbodyに渡し、

items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Foo',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Bar',
          ),
        ],

でボトムナビゲーションのUIを設定してタップアクションを

onTap: (int index) {
          switch (index) {
            case 0:
              const FooRouteData().go(context);
              break;
            case 1:
              const BarRouteData().go(context);
              break;
          }
        },	

swich文で設定しているという感じですね


StatefulShellRouteなるものがあるらしいけどそれはなんなの?

はい、

StatefulShellRouteなんですけどこれもボトムナビゲーションの一つです

こちらが公式のサンプルコードです

https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/stateful_shell_route_example.dart


じゃあ何が違うんだい!って話だと思うんですけど詳しくわかってません💦

私自身使っているのはStatefulShellRouteの方で

記事書くにあたってShellRouteの方を知ったので

ShellRouteの方が実際あんまよくわかってません🙇


恐らくなんですがShellRouteの方が静的にしかタブの状態を持てず、

StatefulShellRouteを使うと動的に各タブの状態を保持できるということだと思ってます


あとはスイッチしなくても簡潔にボトムナビゲーションの画面遷移をかけるとか

そういうメリットもあります(この辺はStatefulで状態を監視している恩恵だと思います)

この辺も詳しい人いたらここも是非コメント欄で教えてもらえると助かります😅

また、

StatefulShellRouteを使うとブランチのネストが深くなって見通しが悪くなるのですが

そちらを分ける方法があるので記事を貼っておきます

https://zenn.dev/pside/articles/9194274980bf76#comment-6bc5e9b55ecdeb

こちらコメント欄に私のコメントがあるのですが

現行のバージョンだとpartしなくても分ける事ができるはずです

ダメだったら記事の方法を参考にしてみてください


まとめ

あれこれつらつら書きましたがgo_router_builderは

記述量を減らせて尚且つ値渡し画面遷移をタイプセーフに行えるので

とてもいいぞ!!!!!!という感じです

地味ですがflutter.devが管理してるパッケージなので

よくわからん3rdパーティじゃないから安心感があるというのも大きいですね

Arsaga Developers Blog

Discussion