【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を使った画面遷移の簡単なサンプルコードを用意しました。
ボタンを押すと押したボタンに応じた画面に遷移する、といった簡単なサンプルです。

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のコードを追記してください。)

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をコピーしました