🛤️

【Flutter】 Navigator から Go Router へ

2023/10/29に公開

初めに

Flutterを始めてから画面遷移に関してはずっとNavigatorを使ってきていたのですが、最近 Go Router の学習を始めました。
Navigator は初め記述方法を覚えるコストがあるものの、アプリ開発をしていれば幾度となく画面遷移の処理を書くことになるので、自然と書き方を覚えていきます。覚えてしまえば直感的に書けるので特に不便は感じていなかったのですが、これを機に Go Router と比較してみたいと思います。

Go Router に触れた後は Go Router Builder にも触れる予定なので、よろしければそちらも併せてご覧ください。

記事の対象者

  • Flutter 学習者
  • Go Routerを使いたい方

実装

導入

go_routerパッケージの最新バージョンを pubspec.yamlに記述

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  go_router: ^12.0.1

または

以下をターミナルで実行

flutter pub add go_router

ルートの指定

router.dartという名前のファイルを作成して、その中に GoRoute の定義をまとめます。

router.dart
import 'package:go_router/go_router.dart';
import 'package:go_router_builder_sample/screens/about_screen.dart';
import 'package:go_router_builder_sample/screens/home_screen.dart';
import 'package:go_router_builder_sample/screens/setting_screen.dart';

final router = GoRouter(
  debugLogDiagnostics: true,
  initialLocation: '/',
  routes: [
    GoRoute(
        name: 'home',
        path: '/',
        builder: (context, state) => const HomeScreen()),
    GoRoute(
        name: 'about',
        path: '/about',
        builder: (context, state) => const AboutScreen()),
    GoRoute(
        name: 'setting',
        path: '/setting',
        builder: (context, state) => const SettingScreen()),
  ],
);

上のコードのように遷移させたいページのルートを指定し、それを router 変数としてグローバルに定義することで他のクラス内でも router を使うことができます。

debugLogDiagnostics: true

debugLogDiagnostics: true,とすることで以下のように、登録されているすべてのルートと遷移した際にどのルートに遷移したかがデバッグコンソールに表示されます。

[GoRouter] Full paths for routes:
             => /
             => /about
             => /setting
           known full paths for route names:
             home => /
             about => /about
             setting => /setting
[GoRouter] setting initial location null
[GoRouter] Using MaterialApp configuration
[GoRouter] going to /about
[GoRouter] going to /setting
[GoRouter] going to /
[GoRouter] going to /about
initialLocation: '/'

initialLocation: '/' のように指定すると、指定されたパスにあるページが最初に表示されるようになります。例えば、initialLocation: '/about' とすると AboutScreen が初めに表示されるようになります。
なお、initialLocation を指定しなかった場合はパスが / になっているものが自動的に一番初めに表示されるページになります。

上の HomeScreenAboutScreenSettingScreen は以下のように指定しておきます。

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Home")),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.go('/about');
          },
          child: const Text(
            "Go To About Screen",
          ),
        ),
      ),
    );
  }
}
about_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('About'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.go('/setting');
          },
          child: const Text(
            'Go To Setting Screen',
          ),
        ),
      ),
    );
  }
}

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Setting')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.go('/');
          },
          child: const Text(
            'Go To Home Screen',
          ),
        ),
      ),
    );
  }
}

各ページには次のページへ進むためのボタンが設けられており、HomeScreenAboutScreenSettingScreenの順番に遷移するようになっています。

main.dart の変更

ルートの指定はできましたが、このままでは指定したルートを使うことはできません。
最後に main.dartを以下のように変更することでルートを使えるようになります。

main.dart
import 'package:flutter/material.dart';
import 'package:go_router_builder_sample/router.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return MaterialApp.router(     // MaterialApp.routerなので注意
      title: 'Go Router Sample',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      // 以下3行を追加
      routerDelegate: router.routerDelegate,
      routeInformationParser: router.routeInformationParser,
      routeInformationProvider: router.routeInformationProvider,
    );
  }
}

先ほどのコードでも示した通り、以下のコードが Navigator と go router を使用した場合に同じような動作になります。

Navigator

home_screen.dart
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const AboutScreen(),
  ),
);

go router

home_screen.dart
context.go('/about');

以上のように context.go('遷移先のパス') として遷移先のパスを引数に入れることで画面遷移を実装することができます。

ただ、go router で遷移した場合には以下のように「戻るボタン」が表示されないという違いがあります。これについては後述します。

Navigator.push go_router
Navigator.push go_router

Navigator

about_screen.dart
Navigator.pop(context);

go router

about_screen.dart
context.pop();

Navigator.pop(context) に関してはそのまま実行してもエラーは起きませんが、context.pop() を実行すると以下のエラーが出力されます。

GoError (GoError: There is nothing to pop)

エラー内容としては書いてある通りで、popするものがないとのことです。
このエラーの原因は、popされる予定だったスクリーンはどのスクリーンの子要素、つまり入れ子ではないために起こっています。
ここで現在の router.dart に以下のような DetailScreen を追加してみましょう。

router.dart
import 'package:go_router/go_router.dart';
import 'package:go_router_builder_sample/screens/about_screen.dart';
import 'package:go_router_builder_sample/screens/detail_screen.dart';
import 'package:go_router_builder_sample/screens/home_screen.dart';
import 'package:go_router_builder_sample/screens/setting_screen.dart';

final router = GoRouter(
  debugLogDiagnostics: true,
  initialLocation: '/',
  routes: [
    GoRoute(
        name: 'home',
        path: '/',
+       routes: [
+         GoRoute(
+           name: 'detail',
+           path: 'detail',
+           builder: (context, state) => const DetailScreen(),
+         )
+       ],
        builder: (context, state) => const HomeScreen()),
    GoRoute(
        name: 'about',
        path: '/about',
        builder: (context, state) => const AboutScreen()),
    GoRoute(
        name: 'setting',
        path: '/setting',
        builder: (context, state) => const SettingScreen()),
  ],
);

GoRouteroutes にさらに GoRoute を指定することで、そのルートの子要素、遷移先として新たなページのルートを指定することができます。

DetailScreen は以下のように AppBar のみの画面にしています。

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Detail"),
      ),
    );
  }
}

以下のように HomeScreen から DetailScreen に遷移するパスを指定すれば下の画像のように「戻るボタン」を持つ DetailScreen に遷移することができます。

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

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

  static String get routeName => 'home';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Home")),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.go('/detail');
          },
          child: const Text(
            "Go To Detail Screen",
          ),
        ),
      ),
    );
  }
}

HomeScreen の入れ子としてルート指定された DetailScreen
detail_screen

Navigator

home_screen.dart
Navigator.pushNamed(context, '/detail');

go router

home_screen.dart
context.goNamed('detail');

goNamedでも階層で下にあるページに関しては「戻るボタン」がある状態で画面に遷移します。

Navigator

home_screen.dart
Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => const DetailScreen(),
  ),
);

go router

home_screen.dart
context.pushReplacement('/detail');

pushReplacementでは現在のページを差し替える形で新たなページに遷移します。
この場合だとHomeScreenを差し替える形でDetailScreenに遷移します。
実際にpushReplacementで遷移すると、これ以上戻るページが存在しないため、以下の画像のように戻るボタンがない状態で遷移します。
push_replacement

値渡し①

まずは DetailScreenuserName というString型の変数を受け取るように変更します。

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

class DetailScreen extends StatelessWidget {
  const DetailScreen({
    super.key, 
+   required this.userName,
  });

+ final String userName;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Detail"),
      ),
+     body: Center(
+       child: Text(
+         "Hello $userName !",
+       ),
+     ),
    );
  }
}

Navigator

home_screen.dart
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const DetailScreen(
      userName: 'Koichi5',
    ),
  ),
);

go router

router.dart
GoRoute(
  name: 'detail',
  path: 'detail',
  builder: (context, state) => DetailScreen(
+   userName: state.extra as String,
  ),
),
home_screen.dart
context.go('/detail', extra: 'Koichi5');

go router では、router.dartDetailScreen に渡す引数を state.extra で指定し、実際に遷移する処理では extraプロパティに渡したい値を指定します。

両方とも以下のように遷移した先で受け取った値を使用することができます。
throw_valiables

値渡し②

router.dart
GoRoute(
  name: 'detail',
  path: 'detail/:user_name/:user_id',
  builder: (context, state) {
    final userName = state.pathParameters['user_name'];
    final userId = state.pathParameters['user_id'];
    return DetailScreen(
      userName: userName!,
      userId: int.parse(userId!),
    );
  },
),
detail_screen.dart
import 'package:flutter/material.dart';

class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key, required this.userName, required this.userId});

  final String userName;
  final int userId;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Detail"),
      ),
      body: Center(
        child: Text(
          "Hello $userName ! \n Your ID is $userId.",
        ),
      ),
    );
  }
}
home_screen.dart
context.go('/detail/Koichi5/101');

以上のように、GoRouteのパスに:変数名を入れることで値を渡すこともできます。
パスとして渡された変数を取り出すためには state.pathParameters['user_name'] のように state.pathParameters で一致する変数名を代入すれば取り出すことができます。
このような渡し方はユーザー固有の情報やリストの詳細情報などに使用されることが多いです。

本記事も https://zenn.dev/articles/50c553218ca938/edit という記事になっており、articles のIDとして50c553218ca938 が与えられていると考えることができ、 GoRouteのパスに指定する方法はこれと似ていると言えます。

実行すると以下のようにユーザー名とIDが正確に渡せていることがわかります。
path_params

まとめ

最後まで読んでいただいてありがとうございました。
今回は Go Router を使った画面遷移が Navigator ではどれに当たるかに焦点を当てて比較してきました。Go Router をタイプセーフで使う Go Router Builder についても学習するつもりなので、よろしければそちらもご覧ください。

参考

https://pub.dev/packages/go_router

https://zenn.dev/channel/articles/af4ffd813b1424

https://zenn.dev/maropook/articles/7cfd45e4a93496

Discussion