go_router 使ってみた（画面間の情報共有）

前回、go_routerを使って単純な画面遷移を実装してみました。
前回の記事はこちら

https://qiita.com/Umigishi-Aoi/items/25ae18293915b017aa4a

今回はもう少し複雑にして、
画面間で情報の受け渡しを行うことを考えたいと思います。

サンプルコードを用意した上で解説しますので、
どのように画面遷移を扱うことになるか、ぜひ見てってください。

##画面遷移の考え方と、状態管理手法##

画面構成は以下のようになっています。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1694109/cab0b18d-09b9-3170-9980-1a23e0c1bbc3.png">

今回実装するのは本のリストページから本の詳細ページへの画面遷移です。
手続的な画面遷移(Navigator 1.0)では、画面遷移時に``Book``オブジェクトを手渡していました。

go_routerを使った画面遷移でもそのまま``Book``オブジェクトを手渡しすることは可能ですが、
この場合、ディープリンク(アドレスバーに直接パスを打ち込んでの画面遷移)はできなくなります。

そのため、以下のような手順を踏んで画面遷移を行います。

1.　遷移のために与えられたパスから、パラメータを取得する
2.　パラメータから対応するオブジェクトを取得する
3.　オブジェクトをページに渡し画面を遷移(構築)する


<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1694109/0c0a57be-ef69-9ee6-04df-d3270c5277db.png">

1.と3.についてはgo_routerにて対応可能ですが、
2.のパラメータから対応するオブジェクトを取得するためには、
オブジェクトのリストをあらかじめ共有しておき、リストから取得する、といった手順が必要となります。
今回はオブジェクトのリストが追加や削除ができる可変なものだと想定し、
リストの状態を状態管理手法(Provider)を使って管理し実装しました。

##サンプルコード##
今回のサンプルコードはこちらになります。

https://github.com/Umigishi-Aoi/go_router_sample/tree/master/go_router_sample

サンプルコードの内容を解説しますので、合わせてご覧ください。

##サブルートの構築##

画面間の情報の受け渡しについて説明する前にサブルートについて説明します。

今回の画面遷移は本のリストページからの遷移です。
詳細画面はリストページ以外からは遷移できません。
このことを強調するために、サブルートにて詳細画面を実装します。

コードは以下のようになります。

```dart:main.dart
//画面の情報を定義する
  final _router = GoRouter(
    initialLocation: '/',
    routes: [

      //省略

      GoRoute(
          name: 'list',
          path: '/list',
          pageBuilder: (context, state) => MaterialPage(
                key: state.pageKey,
                child: const BookListPage(),
              ),
　　　　　　　　　　　　　　　　　　　　//1
          routes: [
            GoRoute(
                name: 'detail',
                //2
                path: 'detail/:id',
                pageBuilder: (context, state) {

　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　  //省略　　　

                }),
          ]),
    ],

    //省略

  );
```

//1
``GoRoute``のインスタンスの中に``routes``を定義することができます。
今回でいえば、``List``のサブルートとして``Detail``が定義されます。
//2
サブルートのパスは'親のパス + サブルートのパス'になります。
今回でいえば、``detail``ページのパスは``/list/detail/1``となります。
(`:id`については後述します。)

##パラメータの取得##

サブルートのパスに``:id``という記載がありました。（//2）
この部分が、画面遷移時に受け渡すパラメータとなります。

例えば、パラメータとして``1``を渡すとしたら、画面遷移のためのコードは以下のようになります。

```dart
context.go('/list/detail/1');
```

``:id``と記載していた場所の文字列がパラメータとして渡されるわけです。

また、もっと明確にパラメータを渡す方法として、以下の遷移の仕方もあります。

```dart
//こちらでも良い
context.goNamed('detail',params: {'id' : '1'});
```

##渡されたパラメータの利用##

上記で渡されたパラメータは、``pageBuilder``の``state``を使用して、
``state.params['id']``として取り出すことが可能です。

サンプルコードの使用例を記載します。

```dart:main.dart
  //画面の情報を定義する
  final _router = GoRouter(

　　　　　　　　　　　　//省略

      GoRoute(

　　　　　　　　　　　　　　　　　　　　//省略

          routes: [
            GoRoute(
                name: 'detail',
                path: 'detail/:id',
                pageBuilder: (context, state) {
　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　//3
                  Book? book = Provider.of<BookList>(context)
                      .bookFromId(state.params['id']!);
                  //4
                  //エラーページへ遷移
                  if (book == null) {
                    return MaterialPage(
                      key: state.pageKey,
                      child: Scaffold(
                          appBar: AppBar(
                            title: Text('ページが存在しません'),
                          ),
                          body: Container()),
                    );
                  }
                  return MaterialPage(
                    key: state.pageKey,
                    //5
                    child: BookDetailPage(
                      book: book,
                    ),
                  );                }),
          ]),
    ],
    
    //省略

  );
```

//3
``BookList``を``Provider``の状態として管理しているため、少し複雑な書き方となっていますが、やっていることはシンプルです。
渡されたパラメータを``state.params['id']``を使って取得し、
``BookList``というオブジェクトから``bookFromId``メソッドを用い``id``の値を使って``Book``オブジェクトを取り出しています。
``bookFromId``メソッドは以下のような実装となります。

```dart:book_list.dart
class BookList {
  final List<Book> books;

  BookList._({required this.books});

  //省略

  Book? bookFromId(String id) {
    try {
      return books.where((element) => element.id.toString() == id).first;
    } catch (e) {
      return null;
    }
  }
}
```

//4
ディープリンクにて遷移した場合、``id``の値に対応した``Book``がない場合があります。
これに対応するため``null``かどうかで条件分岐しています。
//5
取得した``Book``オブジェクトを詳細ページに手渡しています

上記のようにすることで、go_routerを使って値の受け渡しを実装できました。

サンプルコードをchromeにて実行し、

- 本のリスト画面で本のタイトルをタップすることで、対応した詳細画面に移動すること
- 戻るボタンが存在し、押すと前の画面に戻ること
- 本の``id``がアドレスバーのパスに表示されること
- パスを直接打ち込むことで、対応する詳細ページに移動すること

を確認してみてください。

:::note info
Providerの使い方は今回の内容から外れるため、省略します。
サンプルコードにて実装内容を確認してください。
:::

##まとめ##
今回はgo_routerを使った画面遷移にて値を受け渡す方法について解説しました。

ディープリンクを実装する関係から、遷移時に受け渡せるのが``String``だけ、
というのがgo_routerの難しいポイントだと思いました。
ただ、ここさえ乗り越えればかなり簡単に実装可能です。

ぜひ使ってみてください。

##参考##

https://gorouter.dev/

https://zenn.dev/inari_sushio/scraps/01ef7604a4b934

https://pub.dev/packages/provider/install

https://www.flutter-study.dev/firebase-app/provider