【Flutter】GoRouterを使った画面遷移【ソース付】

Flutterの画面遷移は以下の2種類があります。

  • Navigator 1: 命令的
  • Navigator 2: 宣言的

Navigator 1ではWEB対応が不十分でした。WEB対応のためにNavigator 2がリリースされましたが、通常の開発者には難しいと言われていました。そのため、簡易化されたバージョンがリリースされるのではないかと言われていましたが、GoRouterは公式?な簡易的な画面遷移のプラグインになりました。この投稿では、まず最低限はGoRouterが動作する方法を紹介しています。その後に、パラメータを使ったデータ渡しの方法を整理しました。

ソース

ソースは以下にあります
https://github.com/fluttersalon/gorouter/tree/master/lib

インストール

いつもの通り、以下のコマンドを実行して、pubspec.yamlに追加します

flutter pub add go_router
flutter pub get

ファイルの作成

まず、以下の4つのファイルを作成します。

  • main.dart
  • アプリを開始するための、いつものソースです。GoRouterを使うことを宣言しています

  • router.dart
  • ページのpathとページのWidgetの対応づけをします。対応づけを一つのファイルですることで、ページが増えてきても、ソースが見通せるようにしています。(画面遷移の動きは、router.dartを見れば良くなる)

  • page1.dart
  • ページ1画面。初期のページです。初期ページの指定は、router.dartにあります。ボタンを押すことで、ページ2に移動します。

  • page2.dart
  • ページ2画面。遷移先のページです。ページのボタンか、バックボタンを押すことでページ1画面に戻ります。

router.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'page1.dart';
import 'page2.dart';
final router = GoRouter(
  initialLocation: Page1.path,
  routes: [
    GoRoute(
      path: Page1.path,
      pageBuilder: (BuildContext context, GoRouterState? state) => MaterialPage(
        child: Page1(),
      ),
    ),
    GoRoute(
      path: Page2.path,
      pageBuilder: (BuildContext context, GoRouterState? state) => MaterialPage(
        child: Page2(),
      ),
    ),
    // ここに「GoRouterの追加分」を後で追加
  ],
);

GoRouterを定義して、ページのpathと対応するページのWidgetをGoRouteとして、定義していく。それぞれのページにstatic constでpathを定義する。追加のページが増えたら、新たなGoRouteをリストに追加する。

initialLocationに初期起動時に表示するページのpathを設定する。

main.dart

import 'package:flutter/material.dart';
import 'router.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routerConfig: router,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}
  • MaterialApp を MaterialApp.routerに変更
  • home を 削除
  • routerDelegate: router.routerDelegateを追加(10.0.0では、routerConfigのみでOK)
  • routeInformationParser: router.routeInformationParserを追加(10.0.0では、routerConfigのみでOK)
  • routerConfig: routerを追加

page1.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'page2.dart';
class Page1 extends StatefulWidget {
  const Page1({Key? key}) : super(key: key);
  static const path = '/page1';
  @override
  _Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Page1'),
      ),
      body: Column(children:[
        TextButton(
              onPressed: () => GoRouter.of(context).push(Page2.path),
              child: Text('Go to page2'),
        ),
       // ここに、「遷移元のデータ追加分」を追加
      ]),
    );
  }
}
  • ページにstatic constのpathを付ける
  • GoRouter.of(context).push(path)で遷移先を指定する
  • この場合、pushなので、backボタンでも戻れる。GoRouter.of(context).go(path) を使えば、backボタンでは戻れなくできる。

page2.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class Page2 extends StatefulWidget {
  const Page2({Key? key}) : super(key: key);
  static const path = '/page2';
  @override
  _Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Page2'),
      ),
      body: TextButton(
        onPressed: () => GoRouter.of(context).pop(),
        child: Text('back'),
      ),
    );
  }
}
  • GoRouter.of(context).pop() で元のページの戻る。

URLにパラメータを含める

実際に画面遷移に使おうとすると、IDなどを一緒にデータを渡すものです。そちらのやり方を記載いたします。

表示するページ

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

class Page3 extends StatefulWidget {
  Page3(this.mode, {this.id, Key? key}) : super(key: key);

  static const path = '/page3/:mode/:id';
  static const pathWithoutId = '/page3/:mode';

  final String mode;
  final String? id;

  @override
  _Page3State createState() => _Page3State();
}

class _Page3State extends State<Page3> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Page3 [mode=${widget.mode} id=${widget.id ?? 'no  id'}]'),
      ),
      body: TextButton(
        onPressed: () => GoRouter.of(context).pop(),
        child: Text('back'),
      ),
    );
  }
}
  • path, pathWithoutIDでパスを定義しています。
  • パスの中に「:(変数名)」とすることで、パラメータとしてデータを受け取ることができます
  • pathは「/page3/update/1」という感じで、データのIDと処理名をpathに含めています。
  • pathNoIdは「/page3/create」のように、処理名だけで、新規作成時などデータにIDがない状態を想定してます。
  • pathNoIdで来たとき、idは渡されないので、Nullableにしてます。
  • pathの定義をページのクラス内で定義してますが、パラメータがあるとGoRouteのあるファイルに入れた方が見通しが良いかも知れません
  • コンストラクタ経由で、GoRouterから指定の値を受け取ります
  • 受け取ったパラメータの値は、特にすることがないので、とりあえず表示してます。それぞれで処理してください

遷移元のデータ追加分

          TextButton(
            onPressed: () => GoRouter.of(context).push('/page3/update/1'),
            child: Text('Go to page3 mode=update id=1'),
          ),
          TextButton(
            onPressed: () => GoRouter.of(context).push('/page3/new'),
            child: Text('Go to page3 mode=new'),
          ),
  • 「/page3/update/1」「/page3/new」という感じでパスにパラメータを含めて指定する(REST API風)
  • updateや1は変数にして、実装ごとに変更する

GoRouterの追加分

新しいページを追加したので、それに対応するGoRouteをrouter.dartに追加します・


import 'page3.dart';

    GoRoute(
      path: Page3.path,
      builder: (BuildContext context, GoRouterState state) {
        final mode = state.params['mode']!;
        final id = state.params['id'];
        return Page3(mode, id: id);
      },
    ),
    GoRoute(
      path: Page3.pathWithoutId,
      builder: (BuildContext context, GoRouterState state) {
        final mode = state.params['mode']!;
        return Page3(mode);
      },
    ),
  • 対応するパス分、GoRouteを追加する(今回はpage3のデータIDがあるケースとないケースの2つあるから、2つ追加)
  • state.params[パラメータ名]でパラメータをString?で取得できる
  • modeは、絶対にデータが入っている想定のため「!」をつけることで、Nullだった場合は例外を発生させる
  • idはNullかもしれないので、何もつけない

豆知識

flutter runでパスを指定して実行

flutter  run --route=/page3/hoge/100

とすると、直接page3をパラメータ付きで開くことができる。iOSとAndroidでサポートされている。

多言語対応

多言語対応させるには、通常MaterialApp内に設定する。GoRouterの場合、 MaterialApp.router内で設定する。

    return MaterialApp(
      home: MaterialApp.router(
        localizationsDelegates: AppLocalizations.localizationsDelegates,
        supportedLocales: AppLocalizations.supportedLocales,
      ),

多言語化対応については、以下でまとめています。参考にしてください。

【2022年12月版】Flutterの多言語対応のベストプラクティスとハマりどころ

トラブルシューティング

実行時にrouteInformation.state != null が発生した

以下のrouterConfigを確認してください。以前の書き方のままで実行すると、上記のエラーが発生しました。
MaterialApp.router(
      routerConfig: router,
)

まとめ

簡単に画面遷移ができるようになる。ただ、Navigation2で「宣言的に画面を遷移する!」と言っていたが、すっかり命令的な画面遷移になっている。良いのだろうか。

参考

本家
本家のドキュメント
本家のドキュメントの日本語訳
monoさんのつぶやき(flutter run -route)

【Flutter】久しぶりにgo_router触ったらエラーにハマった話