【Flutter】Navigator 2.0 + Riverpod での画面遷移【簡単なサンプルコード付き】

Flutter基礎

この記事は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をお勧めします。

Navigator 2.0は本来宣言的な画面遷移だけではありません。
Web ページを想定した、アドレスバーでの画面遷移などを可能にしています。
ただ、この実装がとてつもなく難しいです。
特に、この後紹介するRiverpodとの組み合わせが思いついていません。
今回はアプリ開発に特化して紹介するため、宣言的な画面遷移のみ取り扱います。

画面遷移のサンプルコード

この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にて公開しています。
ぜひ参考にしてください。

GitHub - Umigishi-Aoi/Navigator2withRiverpod: Sample for using Navigator 2.0 with Riverpod
Sample for using Navigator 2.0 with Riverpod. Contribute to Umigishi-Aoi/Navigator2withRiverpod development by creating an account on GitHub.

まとめ

今回はこのNavigator2.0について、モバイルアプリ開発ならここさえ知っておけばOK!
という内容の解説と、
Riverpodを組み合わせた時のシンプルさについて解説しました。

ここまでならNavigator2.0もそこまで難しくなく扱えると思います。

Riverpodとの組み合わせは頭を悩ましたところでしたが、
宣言的な画面遷移のみのNavigator2.0なら組み合わせは簡単です。

今回のコードが初心者の方の助けになれば幸いです。

コメント

タイトルとURLをコピーしました