この記事は2021年11月25日に更新しました。
こんにちは、海岸 蒼です。
Flutterを使っていて、画面遷移の仕方は常に頭を悩ますところだと思います。
現在、Flutterで推奨されている画面遷移の手法がNavigator2.0 という手法です。
ただNavigator2.0、結構理解するのが難しいですよね。
今回はこのNavigator2.0について、モバイルアプリ開発ならここさえ知っておけばOK!
という内容の解説と、
Riverpodを組み合わせた時のシンプルさについて解説したいと思います。
前提として、Flutter 2.5, Riverpod 1.0.0での解説となります。
あらかじめご了承ください。
それでは早速参りましょう。
Navigator2.0とは
Navigator2.0とは命令的ではなく、宣言的な画面遷移の手法です。
命令的?宣言的?よく分かりませんよね。
図を作成したのでこちらをご覧ください。

命令的な画面遷移はその名の通り、命令を送ることで画面が遷移する手法です。
Navigator1.0での画面遷移がこれにあたります。

宣言的な画面遷移は、画面遷移に関わる状態を変化させることで、それに応じた画面に遷移する、
という画面遷移の手法です。
命令的な方よりまわりくどくなっています。
ただ、状態に情報を持たせられるのは強みです。
状態から情報を読み取って画面を遷移させることが可能となります。
例えば本のリストを作成した場合です。
リストをタップすると本の情報を表示する、といった画面遷移を考えます。
本が追加された場合、命令的な画面遷移では、
追加と同時に本の情報が記載された画面を用意し、その名前を用意し、
画面遷移時にその名前を命令して画面を遷移する、という手順になります。
宣言的な画面遷移では、リストの追加だけしておけばよく、
タップされた際状態として本の情報を受け取り、情報から画面を作り表示、
という手順で画面が遷移できます。
どちらの手法でも可能だと思います。
複雑な画面遷移でなければ好みですが、
情報を受け渡したりする場合はNavigator2.0をお勧めします。
画面遷移のサンプルコード

このNavigator2.0を使った画面遷移の簡単なサンプルコードを用意しました。
ボタンを押すと押したボタンに応じた画面に遷移する、といった簡単なサンプルです。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | import 'package:flutter/material.dart' ; void main() { runApp(const Navigator2test()); } class Navigator2test extends StatefulWidget { const Navigator2test({Key? key}) : super (key: key); @override State<Navigator2test> createState() => _Navigator2testState(); } class _Navigator2testState extends State<Navigator2test> { //表示Pageを管理する状態 //1ならPage1、2ならPage2といった形で管理する int? _buttonId; @override Widget build(BuildContext context) { return MaterialApp( title: 'Books App' , //宣言的な画面遷移の管理部分 //_buttonIdの値によって変化する home: Navigator( pages: [ MaterialPage( child: MenuPage( toPage1: _toPage1, toPage2: _toPage2, toPage3: _toPage3, ), ), if (_buttonId == 1) const MaterialPage( child: Page1(), ), if (_buttonId == 2) const MaterialPage( child: Page2(), ), if (_buttonId == 3) const MaterialPage( child: Page3(), ), ], onPopPage: (route, result) { if (!route.didPop(result)) { return false ; } setState(() { _buttonId = null ; }); return true ; }, ), ); } //ページ遷移のためのコールバック関数 //管理している状態の数値を変更する void _toPage1() { setState(() { _buttonId = 1; }); } void _toPage2() { setState(() { _buttonId = 2; }); } void _toPage3() { setState(() { _buttonId = 3; }); } } class MenuPage extends StatelessWidget { const MenuPage({Key? key, this .toPage1, this .toPage2, this .toPage3}) : super (key: key); //画面遷移を指示するコールバック関数 //ボタンが押された時に反応する final void Function()? toPage1; final void Function()? toPage2; final void Function()? toPage3; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Menu Page' ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton(onPressed: toPage1, child: const Text( 'Page1' )), ElevatedButton(onPressed: toPage2, child: const Text( 'Page2' )), ElevatedButton(onPressed: toPage3, child: const Text( 'Page3' )), ], ), ), ); } } class Page1 extends StatelessWidget { const Page1({Key? key}) : super (key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Page1' ), ), backgroundColor: Colors.red, body: const Center( child: Text( 'More Information Here' ), ), ); } } class Page2 extends StatelessWidget { const Page2({Key? key}) : super (key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Page2' ), ), backgroundColor: Colors.blue, body: const Center( child: Text( 'More Information Here' ), ), ); } } class Page3 extends StatelessWidget { const Page3({Key? key}) : super (key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Page3' ), ), backgroundColor: Colors.green, body: const Center( child: Text( 'More Information Here' ), ), ); } } |
ポイントは25行目からのNavigator~の部分ですね。
ここが宣言的な画面遷移を指示している部分となります。
buttonIdの値が何かによって表示される画面を変えているわけです。
ただ、このサンプルコード、動くんですが、ちょっと複雑になっています。
複雑な部分はMenuPageのコールバック関数です。
Navigator2testクラスの状態をMenuPageから変化させる以上、
コールバック関数を使って変化させるしかありません。
これって手間だし、まわりくどいですよね。
どうせならMenuPageから直接Navigator2testクラスの状態を変化させたいです。
そんないい方法があるでしょうか、、、あります。
ということで紹介するのがRiverpodを組み合わせた例です。
Navigator2.0 + Riverpodのサンプルコード

先のアプリをRiverpodを使って書くとこんな感じになります。
(pubspec.yamlにriverpodのコードを追記してください。)
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | import 'package:flutter/material.dart' ; import 'package:flutter_riverpod/flutter_riverpod.dart' ; final buttonIdProvider = StateProvider((ref) => 0); void main() { runApp(const ProviderScope(child: Navigator2test())); } class Navigator2test extends ConsumerWidget { const Navigator2test({Key? key}) : super (key: key); @override Widget build(BuildContext context, WidgetRef ref) { var buttonId = ref.watch(buttonIdProvider); return MaterialApp( title: 'Books App' , //宣言的な画面遷移の管理部分 //buttonIdの値によって変化する home: Navigator( pages: [ const MaterialPage( child: MenuPage(), ), if (buttonId == 1) const MaterialPage( child: Page1(), ), if (buttonId == 2) const MaterialPage( child: Page2(), ), if (buttonId == 3) const MaterialPage( child: Page3(), ), ], onPopPage: (route, result) { if (!route.didPop(result)) { return false ; } ref.read(buttonIdProvider.state).state = 0; return true ; }, ), ); } } class MenuPage extends ConsumerWidget { const MenuPage({Key? key}) : super (key: key); @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar( title: const Text( 'Menu Page' ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () { ref.read(buttonIdProvider.state).state = 1; }, child: const Text( 'Page1' )), ElevatedButton( onPressed: () { ref.read(buttonIdProvider.state).state = 2; }, child: const Text( 'Page2' )), ElevatedButton( onPressed: () { ref.read(buttonIdProvider.state).state = 3; }, child: const Text( 'Page3' )), ], ), ), ); } } class Page1 extends StatelessWidget { const Page1({Key? key}) : super (key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Page1' ), ), backgroundColor: Colors.red, body: const Center( child: Text( 'More Information Here' ), ), ); } } class Page2 extends StatelessWidget { const Page2({Key? key}) : super (key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Page2' ), ), backgroundColor: Colors.blue, body: const Center( child: Text( 'More Information Here' ), ), ); } } class Page3 extends StatelessWidget { const Page3({Key? key}) : super (key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Page3' ), ), backgroundColor: Colors.green, body: const Center( child: Text( 'More Information Here' ), ), ); } } |
ちょっと行間詰めているところがあるかもですが、20行以上短くかけています。
こちらのコードはGitHubにて公開しています。
ぜひ参考にしてください。
まとめ

今回はこのNavigator2.0について、モバイルアプリ開発ならここさえ知っておけばOK!
という内容の解説と、
Riverpodを組み合わせた時のシンプルさについて解説しました。
ここまでならNavigator2.0もそこまで難しくなく扱えると思います。
Riverpodとの組み合わせは頭を悩ましたところでしたが、
宣言的な画面遷移のみのNavigator2.0なら組み合わせは簡単です。
今回のコードが初心者の方の助けになれば幸いです。
コメント