😇

go_router_builder入門

2024/08/05に公開

👤対象者

  • go_router_builderに興味がある人
  • 使ってるけど何を参考に学習したいいかわからない人

今回必要なパッケージはこちら💁
go_router 14.2.2
go_router_builder 2.7.0
build_runner 2.4.11

go_router_builderのpub.devの解説は古いみたいです。公式見ろというが親切ではない。参考になりませんでした。

じゃあ最新版のソースコードはどこにあるのか?
Githubです笑

exampleに、OSSのソースコードあるので、このサンプルコードを参考にわかるまで勉強しましょう。Provider使ったソースコードは見ても参考になりません😇

OSS(オープンソースソフトウェア(Open Source Software))とは、ソースコードが公開されており、無償で誰でも自由に改変、再配布が可能なソフトウェアのことです。

🧑‍🦲「公式ドキュメントを見る」
👨「書いてねーよハゲ」

そうです。書いてないんですね。その時は、探しましょう。あるいは内部実装を見るか😱

pub.devのexample
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs, unreachable_from_main

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import 'shared/data.dart';

part 'main.g.dart';

void main() => runApp(App());

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

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

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<void>(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 (!context.mounted) {
                        return;
                      }
                      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 FamilyScreen extends StatelessWidget {
  const FamilyScreen({required this.family, super.key});
  final Family family;

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(family.name)),
        body: ListView(
          children: <Widget>[
            for (final Person p in family.people)
              ListTile(
                title: Text(p.name),
                onTap: () => PersonRoute(family.id, p.id).go(context),
              ),
          ],
        ),
      );
}

class PersonScreen extends StatelessWidget {
  const PersonScreen({required this.family, required this.person, super.key});

  final Family family;
  final Person person;

  static int _extraClickCount = 0;

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(person.name)),
        body: ListView(
          children: <Widget>[
            ListTile(
              title: Text(
                  '${person.name} ${family.name} is ${person.age} years old'),
            ),
            for (final MapEntry<PersonDetails, String> entry
                in person.details.entries)
              ListTile(
                title: Text(
                  '${entry.key.name} - ${entry.value}',
                ),
                trailing: OutlinedButton(
                  onPressed: () => PersonDetailsRoute(
                    family.id,
                    person.id,
                    entry.key,
                    $extra: ++_extraClickCount,
                  ).go(context),
                  child: const Text('With extra...'),
                ),
                onTap: () => PersonDetailsRoute(family.id, person.id, entry.key)
                    .go(context),
              )
          ],
        ),
      );
}

class PersonDetailsPage extends StatelessWidget {
  const PersonDetailsPage({
    required this.family,
    required this.person,
    required this.detailsKey,
    this.extra,
    super.key,
  });

  final Family family;
  final Person person;
  final PersonDetails detailsKey;
  final int? extra;

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(person.name)),
        body: ListView(
          children: <Widget>[
            ListTile(
              title: Text(
                '${person.name} ${family.name}: '
                '$detailsKey - ${person.details[detailsKey]}',
              ),
            ),
            if (extra == null) const ListTile(title: Text('No extra click!')),
            if (extra != null)
              ListTile(title: Text('Extra click count: $extra')),
          ],
        ),
      );
}

class FamilyCountScreen extends StatelessWidget {
  const FamilyCountScreen({super.key, required this.count});

  final int count;

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('Family Count')),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Center(
                child: Text(
                  'There are $count families',
                  style: Theme.of(context).textTheme.headlineSmall,
                ),
              ),
              ElevatedButton(
                onPressed: () => context.pop(count),
                child: Text('Pop with return value $count'),
              ),
            ],
          ),
        ),
      );
}

class LoginScreen extends StatelessWidget {
  const LoginScreen({this.from, super.key});
  final String? from;

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text(App.title)),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ElevatedButton(
                onPressed: () {
                  // log a user in, letting all the listeners know
                  context.read<LoginInfo>().login('test-user');

                  // if there's a deep link, go there
                  if (from != null) {
                    context.go(from!);
                  }
                },
                child: const Text('Login'),
              ),
            ],
          ),
        ),
      );
}

簡単な画面遷移と詳細ページに値を渡してみる

onTapしたページに値を渡すサンプルで学習をしてみましょう。ダミーのデータいるので、公式のコードをそのまま使います。

データ
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

import 'dart:math';

import 'package:flutter/foundation.dart';

enum PersonDetails {
  hobbies,
  favoriteFood,
  favoriteSport,
}

enum SportDetails {
  volleyball(
    imageUrl: '/sportdetails/url/volleyball.jpg',
    playerPerTeam: 6,
    accessory: null,
    hasNet: true,
  ),
  football(
    imageUrl: '/sportdetails/url/Football.jpg',
    playerPerTeam: 11,
    accessory: null,
    hasNet: true,
  ),
  tennis(
    imageUrl: '/sportdetails/url/tennis.jpg',
    playerPerTeam: 2,
    accessory: 'Rackets',
    hasNet: true,
  ),
  hockey(
    imageUrl: '/sportdetails/url/hockey.jpg',
    playerPerTeam: 6,
    accessory: 'Hockey sticks',
    hasNet: true,
  ),
  ;

  const SportDetails({
    required this.accessory,
    required this.hasNet,
    required this.imageUrl,
    required this.playerPerTeam,
  });

  final String imageUrl;
  final int playerPerTeam;
  final String? accessory;
  final bool hasNet;
}

/// An enum used only in iterables.
enum CookingRecipe {
  burger,
  pizza,
  tacos,
}

/// sample Person class
class Person {
  Person({
    required this.id,
    required this.name,
    required this.age,
    this.details = const <PersonDetails, String>{},
  });

  final int id;
  final String name;
  final int age;

  final Map<PersonDetails, String> details;
}

class Family {
  Family({required this.id, required this.name, required this.people});

  final String id;
  final String name;
  final List<Person> people;

  Person person(int pid) => people.singleWhere(
        (Person p) => p.id == pid,
        orElse: () => throw Exception('unknown person $pid for family $id'),
      );
}

final List<Family> familyData = <Family>[
  Family(
    id: 'f1',
    name: 'Sells',
    people: <Person>[
      Person(id: 1, name: 'Chris', age: 52, details: <PersonDetails, String>{
        PersonDetails.hobbies: 'coding',
        PersonDetails.favoriteFood: 'all of the above',
        PersonDetails.favoriteSport: 'football?'
      }),
      Person(id: 2, name: 'John', age: 27),
      Person(id: 3, name: 'Tom', age: 26),
    ],
  ),
  Family(
    id: 'f2',
    name: 'Addams',
    people: <Person>[
      Person(id: 1, name: 'Gomez', age: 55),
      Person(id: 2, name: 'Morticia', age: 50),
      Person(id: 3, name: 'Pugsley', age: 10),
      Person(id: 4, name: 'Wednesday', age: 17),
    ],
  ),
  Family(
    id: 'f3',
    name: 'Hunting',
    people: <Person>[
      Person(id: 1, name: 'Mom', age: 54),
      Person(id: 2, name: 'Dad', age: 55),
      Person(id: 3, name: 'Will', age: 20),
      Person(id: 4, name: 'Marky', age: 21),
      Person(id: 5, name: 'Ricky', age: 22),
      Person(id: 6, name: 'Danny', age: 23),
      Person(id: 7, name: 'Terry', age: 24),
      Person(id: 8, name: 'Mikey', age: 25),
      Person(id: 9, name: 'Davey', age: 26),
      Person(id: 10, name: 'Timmy', age: 27),
      Person(id: 11, name: 'Tommy', age: 28),
      Person(id: 12, name: 'Joey', age: 29),
      Person(id: 13, name: 'Robby', age: 30),
      Person(id: 14, name: 'Johnny', age: 31),
      Person(id: 15, name: 'Brian', age: 32),
    ],
  ),
];

Family familyById(String fid) => familyData.family(fid);

extension on List<Family> {
  Family family(String fid) => singleWhere(
        (Family f) => f.id == fid,
        orElse: () => throw Exception('unknown family $fid'),
      );
}

class LoginInfo extends ChangeNotifier {
  String _userName = '';
  String get userName => _userName;
  bool get loggedIn => _userName.isNotEmpty;

  void login(String userName) {
    _userName = userName;
    notifyListeners();
  }

  void logout() {
    _userName = '';
    notifyListeners();
  }
}

class FamilyPerson {
  FamilyPerson({required this.family, required this.person});

  final Family family;
  final Person person;
}

class Repository {
  static final Random rnd = Random();

  Future<List<Family>> getFamilies() async {
    // simulate network delay
    await Future<void>.delayed(const Duration(seconds: 1));

    // simulate error
    // if (rnd.nextBool()) throw Exception('error fetching families');

    // return data "fetched over the network"
    return familyData;
  }

  Future<Family> getFamily(String fid) async =>
      (await getFamilies()).family(fid);

  Future<FamilyPerson> getPerson(String fid, int pid) async {
    final Family family = await getFamily(fid);
    return FamilyPerson(family: family, person: family.person(pid));
  }
}

go_router_builderの特徴

  • パスを書くときは自動生成してくれるので短いコードでかけるみたいだ
  • 設定間違えないとコマンド実行した時に失敗するので注意
  • Type-safeに値を渡せる。なんだそれ?

パスはこれだけ:

<HomeRoute>(
  path: '/',
  name: 'Home',
  routes: <TypedGoRoute<GoRouteData>>[
    TypedGoRoute<FamilyRoute>(path: 'family/:familyId')
  ],
)

Type-safe routes topic

Instead of using URL strings to navigate, go_router supports type-safe routes using the go_router_builder package.

To get started, add go_router_builder, build_runner, and build_verify to the dev_dependencies section of your pubspec.yaml:

URL 文字列を使用して移動する代わりに、go_router は go_router_builder パッケージを使用してタイプセーフ ルートをサポートします。 まず、go_router_builder、build_runner、および build_verify を pubspec.yaml の dev_dependency セクションに追加します。

VScodeなら、Runボタン押せば、main.dartのようなファイルは複数作ってもボタンを押したファイルで実行できるので、ビルドできます。公式と同じsimple_example.dartを作成しましょう。他のも同じように作って試してみてください。

example
simple_example.dart
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs, unreachable_from_main

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_builder_tutorial/shared/data.dart';


part 'simple_example.g.dart';

void main() => runApp(App());

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

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

  final GoRouter _router = GoRouter(routes: $appRoutes);
}

<HomeRoute>(
  path: '/',
  name: 'Home',
  routes: <TypedGoRoute<GoRouteData>>[
    TypedGoRoute<FamilyRoute>(path: 'family/:familyId')
  ],
)
class HomeRoute extends GoRouteData {
  const HomeRoute();

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

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

  final String familyId;

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

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

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text(_appTitle)),
        body: ListView(
          children: <Widget>[
            for (final Family family in familyData)
              ListTile(
                title: Text(family.name),
                onTap: () => FamilyRoute(family.id).go(context),
              )
          ],
        ),
      );
}

class FamilyScreen extends StatelessWidget {
  const FamilyScreen({required this.family, super.key});
  final Family family;

  
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(family.name)),
        body: ListView(
          children: <Widget>[
            for (final Person p in family.people)
              ListTile(
                title: Text(p.name),
              ),
          ],
        ),
      );
}

const String _appTitle = 'GoRouter Example: builder';

ファイルを自動生成しないと使えませんので以下のコマンドを実行する。

flutter pub run build_runner watch --delete-conflicting-outputs

次のページに値を渡すときに、go_routerだとStringしか渡せないので、extraを使う。これが使いずらい😅

でも、go_router_builderは書かなくていい。型安全に扱えるTypeSafeなるものだから。ありがたいね 💙🤎

ListTile(
    title: Text(family.name),
    onTap: () => FamilyRoute(family.id).go(context),
)

画面遷移はこんな感じ:


StatefulShellRouteDataを使う

BottomNavigationBarを使うと思ってくれれば良いです。タブメニューで画面遷移するときに使うようだ。
内部実装を見たら、ここのリンクがあった。
StatefulShellRoute class

UIシェルを表示するルートで、サブルート用に別々のナビゲータを持ちます。

ShellRouteと同様に、このルートクラスはルートNavigatorとは異なるNavigatorにサブルートを配置します。しかし、このルートクラスはネストされたブランチ(つまり並列ナビゲーションツリー)ごとに別々のナビゲータを作成する点が異なり、ステートフルなネストされたナビゲーションを持つアプリを構築することができます。これは、例えばBottomNavigationBarを持つUIを実装し、各タブの永続的なナビゲーション状態を持つ場合に便利です。

StatefulShellRouteは、StatefulShellBranchアイテムのリストを指定することで作成されます。StatefulShellBranchは、ルートルートとブランチのナビゲータキー(GlobalKey)、およびオプションの初期位置を提供します。

ShellRouteと同様に、StatefulShellRouteを作成する際にはビルダーかpageBuilderのどちらかを提供する必要があります。ただし、これらのビルダーは子ウィジェットの代わりに StatefulNavigationShell パラメータを受け付ける点が若干異なります。StatefulNavigationShell は、ルートの状態に関する情報にアクセスしたり、アクティブなブランチを切り替える(別のブランチのナビゲーションスタックを復元する)ために使用できます。後者は、例えばStatefulNavigationShell.goBranchメソッドを使用することで実現できます:

出そうだ😅

go_router使ってるとこんなのがよく出てくる???

final GlobalKey<NavigatorState> _sectionANavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'sectionANav');

Globalkeyとは、異なる画面に同じウィジェットを使用し、すべて同じ状態にしたい場合ある値を他のウィジェットから取得したい場合に使う。タブメニューで使うには、これが必要ということか。

内部実装に書かれていたコード。どのルートを通ったか追跡するのか?

/// サブツリーが処分されるまで処分を待つエントリーのセット。
/// 廃棄される。
///
/// これらのエントリは_history内にあるとはみなされず、通常、 /// 廃棄できるようになるとこのセットから削除される。
これらのエントリは_history内にあるとはみなされず、 /// 通常は、破棄できるようになると、このセットから削除されます。
///
/// ナビゲータはこれらのエントリを追跡しておく。
/// ナビゲータはこれらのエントリを追跡しておく。

これだけ書いて、自動生成のコードを実行すると、タブメニューがあるサンプルを作れます。

example
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs, unreachable_from_main

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

part 'stateful_shell_route_example.g.dart';

final GlobalKey<NavigatorState> _sectionANavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'sectionANav');

void main() => runApp(App());

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

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

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

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

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

<MyShellRouteData>(
  branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
    TypedStatefulShellBranch<BranchAData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<DetailsARouteData>(path: '/detailsA'),
      ],
    ),
    TypedStatefulShellBranch<BranchBData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<DetailsBRouteData>(path: '/detailsB'),
      ],
    ),
  ],
)
class MyShellRouteData extends StatefulShellRouteData {
  const MyShellRouteData();

  
  Widget builder(
    BuildContext context,
    GoRouterState state,
    StatefulNavigationShell navigationShell,
  ) {
    return navigationShell;
  }

  static const String $restorationScopeId = 'restorationScopeId';

  static Widget $navigatorContainerBuilder(BuildContext context,
      StatefulNavigationShell navigationShell, List<Widget> children) {
    return ScaffoldWithNavBar(
      navigationShell: navigationShell,
      children: children,
    );
  }
}

class BranchAData extends StatefulShellBranchData {
  const BranchAData();
}

class BranchBData extends StatefulShellBranchData {
  const BranchBData();

  static final GlobalKey<NavigatorState> $navigatorKey = _sectionANavigatorKey;
  static const String $restorationScopeId = 'restorationScopeId';
}

class DetailsARouteData extends GoRouteData {
  const DetailsARouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const DetailsScreen(label: 'A');
  }
}

class DetailsBRouteData extends GoRouteData {
  const DetailsBRouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const DetailsScreen(label: 'B');
  }
}

/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class ScaffoldWithNavBar extends StatelessWidget {
  /// Constructs an [ScaffoldWithNavBar].
  const ScaffoldWithNavBar({
    required this.navigationShell,
    required this.children,
    Key? key,
  }) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));

  /// The navigation shell and container for the branch Navigators.
  final StatefulNavigationShell navigationShell;

  /// The children (branch Navigators) to display in a custom container
  /// ([AnimatedBranchContainer]).
  final List<Widget> children;

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedBranchContainer(
        currentIndex: navigationShell.currentIndex,
        children: children,
      ),
      bottomNavigationBar: BottomNavigationBar(
        // Here, the items of BottomNavigationBar are hard coded. In a real
        // world scenario, the items would most likely be generated from the
        // branches of the shell route, which can be fetched using
        // `navigationShell.route.branches`.
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
          BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
        ],
        currentIndex: navigationShell.currentIndex,
        onTap: (int index) => _onTap(context, index),
      ),
    );
  }

  /// Navigate to the current location of the branch at the provided index when
  /// tapping an item in the BottomNavigationBar.
  void _onTap(BuildContext context, int index) {
    // When navigating to a new branch, it's recommended to use the goBranch
    // method, as doing so makes sure the last navigation state of the
    // Navigator for the branch is restored.
    navigationShell.goBranch(
      index,
      // A common pattern when using bottom navigation bars is to support
      // navigating to the initial location when tapping the item that is
      // already active. This example demonstrates how to support this behavior,
      // using the initialLocation parameter of goBranch.
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}

/// Custom branch Navigator container that provides animated transitions
/// when switching branches.
class AnimatedBranchContainer extends StatelessWidget {
  /// Creates a AnimatedBranchContainer
  const AnimatedBranchContainer(
      {super.key, required this.currentIndex, required this.children});

  /// The index (in [children]) of the branch Navigator to display.
  final int currentIndex;

  /// The children (branch Navigators) to display in this container.
  final List<Widget> children;

  
  Widget build(BuildContext context) {
    return Stack(
        children: children.mapIndexed(
      (int index, Widget navigator) {
        return AnimatedScale(
          scale: index == currentIndex ? 1 : 1.5,
          duration: const Duration(milliseconds: 400),
          child: AnimatedOpacity(
            opacity: index == currentIndex ? 1 : 0,
            duration: const Duration(milliseconds: 400),
            child: _branchNavigatorWrapper(index, navigator),
          ),
        );
      },
    ).toList());
  }

  Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer(
        ignoring: index != currentIndex,
        child: TickerMode(
          enabled: index == currentIndex,
          child: navigator,
        ),
      );
}

/// The details screen for either the A or B screen.
class DetailsScreen extends StatefulWidget {
  /// Constructs a [DetailsScreen].
  const DetailsScreen({
    required this.label,
    this.param,
    this.extra,
    super.key,
  });

  /// The label to display in the center of the screen.
  final String label;

  /// Optional param
  final String? param;

  /// Optional extra object
  final Object? extra;
  
  State<StatefulWidget> createState() => DetailsScreenState();
}

/// The state for DetailsScreen
class DetailsScreenState extends State<DetailsScreen> {
  int _counter = 0;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details Screen - ${widget.label}'),
      ),
      body: _build(context),
    );
  }

  Widget _build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text('Details for ${widget.label} - Counter: $_counter',
              style: Theme.of(context).textTheme.titleLarge),
          const Padding(padding: EdgeInsets.all(4)),
          TextButton(
            onPressed: () {
              setState(() {
                _counter++;
              });
            },
            child: const Text('Increment counter'),
          ),
          const Padding(padding: EdgeInsets.all(8)),
          if (widget.param != null)
            Text('Parameter: ${widget.param!}',
                style: Theme.of(context).textTheme.titleMedium),
          const Padding(padding: EdgeInsets.all(8)),
          if (widget.extra != null)
            Text('Extra: ${widget.extra!}',
                style: Theme.of(context).textTheme.titleMedium),
        ],
      ),
    );
  }
}

initialLocationを使う

これは普通のgo_routerの機能ですね。最初に表示するページを指定します。

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

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

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

ページ3個分のルートを定義します。今回は、TabBarもある。

<MainShellRouteData>(
  branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
    TypedStatefulShellBranch<HomeShellBranchData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<HomeRouteData>(
          path: '/home',
        ),
      ],
    ),
    TypedStatefulShellBranch<NotificationsShellBranchData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<NotificationsRouteData>(
          path: '/notifications/:section',
        ),
      ],
    ),
    TypedStatefulShellBranch<OrdersShellBranchData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<OrdersRouteData>(
          path: '/orders',
        ),
      ],
    ),
  ],
)

TabBarは、パッケージとは別の標準の機能で実装するようですね。

class NotificationsPageView extends StatelessWidget {
  const NotificationsPageView({
    super.key,
    required this.section,
  });

  final NotificationsPageSection section;

  
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      initialIndex: NotificationsPageSection.values.indexOf(section),
      child: const Column(
        children: <Widget>[
          TabBar(
            tabs: <Tab>[
              Tab(
                child: Text(
                  'Latest',
                  style: TextStyle(color: Colors.black87),
                ),
              ),
              Tab(
                child: Text(
                  'Old',
                  style: TextStyle(color: Colors.black87),
                ),
              ),
              Tab(
                child: Text(
                  'Archive',
                  style: TextStyle(color: Colors.black87),
                ),
              ),
            ],
          ),
          Expanded(
            child: TabBarView(
              children: <Widget>[
                NotificationsSubPageView(
                  label: 'Latest notifications',
                ),
                NotificationsSubPageView(
                  label: 'Old notifications',
                ),
                NotificationsSubPageView(
                  label: 'Archived notifications',
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

全体のコードがこちらですね。コードが多すぎるような複雑なような。

example
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs, unreachable_from_main

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

part 'stateful_shell_route_initial_location_example.g.dart';

void main() => runApp(App());

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

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

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

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

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

<MainShellRouteData>(
  branches: <TypedStatefulShellBranch<StatefulShellBranchData>>[
    TypedStatefulShellBranch<HomeShellBranchData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<HomeRouteData>(
          path: '/home',
        ),
      ],
    ),
    TypedStatefulShellBranch<NotificationsShellBranchData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<NotificationsRouteData>(
          path: '/notifications/:section',
        ),
      ],
    ),
    TypedStatefulShellBranch<OrdersShellBranchData>(
      routes: <TypedRoute<RouteData>>[
        TypedGoRoute<OrdersRouteData>(
          path: '/orders',
        ),
      ],
    ),
  ],
)
class MainShellRouteData extends StatefulShellRouteData {
  const MainShellRouteData();

  
  Widget builder(
    BuildContext context,
    GoRouterState state,
    StatefulNavigationShell navigationShell,
  ) {
    return MainPageView(
      navigationShell: navigationShell,
    );
  }
}

class HomeShellBranchData extends StatefulShellBranchData {
  const HomeShellBranchData();
}

class NotificationsShellBranchData extends StatefulShellBranchData {
  const NotificationsShellBranchData();

  static String $initialLocation = '/notifications/old';
}

class OrdersShellBranchData extends StatefulShellBranchData {
  const OrdersShellBranchData();
}

class HomeRouteData extends GoRouteData {
  const HomeRouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const HomePageView(label: 'Home page');
  }
}

enum NotificationsPageSection {
  latest,
  old,
  archive,
}

class NotificationsRouteData extends GoRouteData {
  const NotificationsRouteData({
    required this.section,
  });

  final NotificationsPageSection section;

  
  Widget build(BuildContext context, GoRouterState state) {
    return NotificationsPageView(
      section: section,
    );
  }
}

class OrdersRouteData extends GoRouteData {
  const OrdersRouteData();

  
  Widget build(BuildContext context, GoRouterState state) {
    return const OrdersPageView(label: 'Orders page');
  }
}

class MainPageView extends StatelessWidget {
  const MainPageView({
    required this.navigationShell,
    super.key,
  });

  final StatefulNavigationShell navigationShell;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite),
            label: 'Notifications',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            label: 'Orders',
          ),
        ],
        currentIndex: navigationShell.currentIndex,
        onTap: (int index) => _onTap(context, index),
      ),
    );
  }

  void _onTap(BuildContext context, int index) {
    navigationShell.goBranch(
      index,
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}

class HomePageView extends StatelessWidget {
  const HomePageView({
    required this.label,
    super.key,
  });

  final String label;

  
  Widget build(BuildContext context) {
    return Center(
      child: Text(label),
    );
  }
}

class NotificationsPageView extends StatelessWidget {
  const NotificationsPageView({
    super.key,
    required this.section,
  });

  final NotificationsPageSection section;

  
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      initialIndex: NotificationsPageSection.values.indexOf(section),
      child: const Column(
        children: <Widget>[
          TabBar(
            tabs: <Tab>[
              Tab(
                child: Text(
                  'Latest',
                  style: TextStyle(color: Colors.black87),
                ),
              ),
              Tab(
                child: Text(
                  'Old',
                  style: TextStyle(color: Colors.black87),
                ),
              ),
              Tab(
                child: Text(
                  'Archive',
                  style: TextStyle(color: Colors.black87),
                ),
              ),
            ],
          ),
          Expanded(
            child: TabBarView(
              children: <Widget>[
                NotificationsSubPageView(
                  label: 'Latest notifications',
                ),
                NotificationsSubPageView(
                  label: 'Old notifications',
                ),
                NotificationsSubPageView(
                  label: 'Archived notifications',
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class NotificationsSubPageView extends StatelessWidget {
  const NotificationsSubPageView({
    required this.label,
    super.key,
  });

  final String label;

  
  Widget build(BuildContext context) {
    return Center(
      child: Text(label),
    );
  }
}

class OrdersPageView extends StatelessWidget {
  const OrdersPageView({
    required this.label,
    super.key,
  });

  final String label;

  
  Widget build(BuildContext context) {
    return Center(
      child: Text(label),
    );
  }
}

最初は、Homeが表示されるようです。

真ん中で、TabBarが使えるページが表示されます。

感想

お疲れ様でした〜
長かったですね。これで皆さんも画面遷移とタブメニューなら作れるようになったはず。

そもそも私は、このパッケージが大嫌いです笑
仕事で使ったことあるけど、使いにくいしgo_routerのバージョンが新しいと使えないことがありますね。安定したものを求めるなら、普通のgo_routerを使うことをお勧めします。私は最近は、auto_routeにハマっておりますが😅

Discussion