LoginSignup
10
10

More than 1 year has passed since last update.

Riverpod で Navigator 2.0 の画面遷移の実装 1

Posted at

最初に

この記事について

自分なりに、汎用的に使うことのできる画面遷移を、状態管理アプリのRiverpodと絡めて実装する方法を紹介します。
とりあえず、モバイルで実装のテンプレート的なものがあればというのを目的に書いた記事なので、Webの対応は今回はしません。

今回作るもの

nav__1.gif

Riverpodについて

Riverpod は知っている方も多いかと思いますが、かなりメジャーな状態管理用パッケージです。ちなみに、同じくらいメジャーな状態管理パッケージの Provider と同じ作者の方が作成しており、Riverpod はその改良版のような立ち位置にあります。

最近だとドキュメントが日本語対応されたりしているので、触れたことがない方も始めるいい機会かと思います。

Navigator 2.0について

Flutterはクロスプラットフォーム開発が売りのフレームワークですが、それに伴ってかなり複雑な構造になり、従来の Navigator では存在した問題を解決するために作られたものが Navigator 2.0 です。

しかし、実はNavigator 2.0はそのまま扱うにはかなり難しい内容で、それを理解したつよつよエンジニアが自分のような開発者のために使いやすいパッケージを開発しているのが現状です。

ただ、いつまでもつよつよエンジニアの方に頼ってばかりではいけない!と思い立ち、記事を読んで自分なりに使いやすい方法を見つけました(かなり暫定的なものなので間違っていたり、おすすめのやり方があったらコメントで教えてください!)。

なので、今回はざっくりとした Navigator 2.0 の解説と Riverpod を絡めた実践的な実装方法を紹介したいと思います。

Navigator 2.0 の解説

今回はRiverpodを絡めた実装をメインに紹介するため、詳しくは解説しません。
もっと詳しく知りたい方は以下の記事がおすすめです。

何が変わったの?

まず従来の Navigator は命令型で 2.0 は状態型という点です。

今までの画面遷移の方法だとpush()pop()などで命令していました。

// ↓ HogeScreen をプッシュしろ!
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const HogeScreen()));

では 2.0 ではどうなったかというと、

Navigator(
  // ページのスタックの変化を監視して、変わっていたら画面遷移をする
  pages: [
    const MaterialPage(child: HogeScreen()),
    if (id.isNotEmpty)
      const MaterialPage(child: NextScreen()),
  ],
)

このようにスタックを用意しておいて、条件などからスタックの状態を変化させます。その変化を内部で監視して、画面遷移をします。
なので状態型と呼ばれているんですね!

こうすることで何がいいかというと、今までに比べて履歴の管理が簡単になりました。
popUntil()とかpushAndRemoveUntil()を駆使しないとできない実装に悩まれた経験、皆さんにもあると思います。それももちろん同じように解決できます。

RouterDelegate

Navigator 2.0を使うにはRouterDelegateを実装する必要があります。
ここでスタックの状態を管理して画面を遷移させるか判断します。
自分で実装するのはpagesくらいなので、そこまで実装は手間ではないです。
今回はWebに対応させないため、全部の機能を使わないので省略できる部分の説明はしません。

router_delegate.dart
// 1
class AppRouterDelegate extends RouterDelegate<AppRoute>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin {
  AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  // 2
  @override
  final GlobalKey<NavigatorState>? navigatorKey;

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      // 3
      pages: [
        const MaterialPage(child: HogeScreen()),
      ],
      // 4
      onPopPage: (route, result) {
        return true;
      },
    );
  }

  // 5
  @override
  Future<void> setNewRoutePath(AppRoute configuration) async {}
}
  1. RouterDelegate の実装には他に ChangeNotifier PopNavigatorRouterDelegateMixinが必要です。ChangeNotifier は状態の更新用に、 PopNavigatorRouterDelegateMixin は pop 時に処理をすることができる popRoute を実装できるようになります。ただ、今回の実装では状態の管理を Riverpod で行うため、 ChangeNotifier 要素は使いません。また、ジェネリクスを指定していますが、これも今回は使いません。

  2. この RouterDelegate で使う NavigatorKey です。コンストラクターで初期化します。

  3. ここが主に自分でカスタマイズする部分になります。ユーザーのアクションなどに応じてスタックを変更することで自動的に画面が遷移します。そして、ここにはPageを継承したWidgetしかおけません。ほとんどのユースケースではMaterialPageで大丈夫です。

  4. pop時に渡される値などを処理できます。trueを返すと常にpop可能になります。falseを返してあげると pop されなくなります。

  5. Web のみ使う機能なので今回は省略します。

また、RouterDelegate の他にRouteInformationParserという Web で URL と Flutter 側の画面情報を相互に変換するためのクラスがありますが、今回はモバイルのみなので省略します。

なので、アプリだけを作りたい!という方は上記の知識のみを知っていれば大丈夫です。

いざ実装

とても簡単なシンプル実装は調べるとよく載っているので割愛して、いきなり Riverpod を絡めた実装になります。

パッケージ・バージョン

Flutterのバージョンは2.8.1です
パッケージは以下の通りです

pubspec.yaml
environment:
  sdk: ">=2.12.0 <3.0.0"
  flutter: ">=2.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^1.0.3

RouterDelegate の作成

RouterDelegateの枠組みを作っていきます
また、エラー回避のためジェネリクスを指定していますが、使いません。

AppRouterDelegate
// 今回の実装では AppRouterDelegate がリビルドされる可能性があるためグローバルに宣言
final _navigatorKey = GlobalKey<NavigatorState>();

class AppRouterDelegate extends RouterDelegate<Empty>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<Empty> {
  @override
  final GlobalKey<NavigatorState>? navigatorKey = _navigatorKey;

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        // ここにページを置く
      ],
      onPopPage: (route, result) => true,
    );
  }

  @override
  Future<void> setNewRoutePath(Empty configuration) async {}
}

class Empty {}

これらを WidgetTree に置くためにはRouter()ウィジェットを使用してください。
使い方は簡単です。
じつはそれ用にMaterialApp.routerが用意されているのですが、 Web のみで使う parser が必須なので Router で代用します。

main.dart
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Router(
        routerDelegate: AppRouterDelegate(), // ここ
      ),
    );
  }
}

Riverpodの導入

セットアップ

ここでようやく Riverpod の登場です。まずは、Riverpod 用にセットアップします

main.dart
void main() {
  runApp(
    const ProviderScope(child: MyApp()),
  );
}

ConsumerWidgetを継承

次にMyAppConsumerWidgetを継承します。
ついでにAppRouterDelegateにWidgetRefを渡します。

main.dart
class MyApp extends ConsumerWidget { // ここ
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) { // ここ
    return MaterialApp(
      home: Router(
        routerDelegate: AppRouterDelegate(ref), // 渡す
      ),
    );
  }
}

class AppRouterDelegate extends RouterDelegate<Empty>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<Empty> {
  AppRouterDelegate(this.ref);
  final WidgetRef ref; // 渡される

  ...
}

ページを作成

次にページを作成して、Navigatorに置きます

main.dart
class AppRouterDelegate extends RouterDelegate<Empty>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<Empty> {
  ...

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        const MaterialPage(child: HogeScreen()),
        const MaterialPage(child: FugaScreen())
      ],
      onPopPage: (route, result) {
        return route.didPop(result);
      },
    );
  }

  ...
}

class HogeScreen extends StatelessWidget {
  const HogeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hogeee'),
      ),
    );
  }
}

class FugaScreen extends StatelessWidget {
  const FugaScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Fugaaaaa'),
        backgroundColor: Colors.redAccent, // わかりやすいように色付け
      ),
    );
  }
}

id を StateProvider で作る

ただこれだと常にFugaScreen()がスタックの上に来てしまうので遷移ができません。
ここで、StateProviderを使って、擬似的なidを作成しましょう。

id_provider.dart
final idProvider = StateProvider<String>((ref) {
  return '';
});

そして、idが空ではない時にFugaScreen()に遷移するようにします。

class AppRouterDelegate extends RouterDelegate<Empty>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<Empty> {
  AppRouterDelegate(this.ref);
  final WidgetRef ref;

  @override
  final GlobalKey<NavigatorState>? navigatorKey = _navigatorKey;

  // 1
  String get id => ref.watch(idProvider);
  StateController<String> get provider => ref.read(idProvider.notifier);

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        const MaterialPage(child: HogeScreen()),
        // 2
        if (id.isNotEmpty) const MaterialPage(child: FugaScreen())
      ],
      onPopPage: (route, result) {
        // 3
        provider.state = '';
        return route.didPop(result);
      },
    );
  }

  ...
}
  1. 値を監視するために watch で宣言。また、id の変更ように read で provider を宣言。今回はモバイルのみの対応になっているので、watchを使って変更を監視していますが、Webも対応する場合、内部でChangeNotifierを使っているので、以下のように実装した方がいいです。
  AppRouterDelegate(this.ref) {
    ref.listen<int?>(idProvider, (_, __) => notifyListeners());
  }
  1. idが空ではない時にFugaScreen()を表示

  2. 戻る時はスタックをHogeScreen()のみにしなければいけないので、idを空にする。

遷移用のボタンを追加

また、遷移するために、HogeScreen()にボタンを追加しましょう。

class HogeScreen extends ConsumerWidget {
  const HogeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hogeee'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('次へ'),
          onPressed: () => ref.read(idProvider.notifier).state = 'id',
        ),
      ),
    );
  }
}

完成🎉

ここまでついてきていますでしょうか?
これで完成です!ビルドしてみましょう!

nav__1.gif

きれいにできました!

まとめ

個人的にはページのスタックを自由にいじれるようになったので、前のNavigatorよりも好印象です
今回の実装も簡単な内容でしたが、かなり拡張性があって、StateProviderだけでなく、StateNotifierProviderとfreezedなんかを使うこともできます。

なので、次回はその実装と、BottomTabなどを使ったNestedNavigationなどの解説・実装の記事をあげようと思います。

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10