FlutterのNavigatorの使い方を基礎から解説
はじめに
皆さんはFlutterで複数の画面を扱うとき、どのように画面を切り替えればよいか迷った経験はないでしょうか。 アプリが一つの画面だけで完結するケースは少なく、多くの場合は複数の画面を行き来する必要があります。 その画面遷移を管理するうえで欠かせないのが、Flutter Navigator という仕組みです。 本記事では、初心者の方にもわかるように、Navigatorを使った画面遷移の方法と、実務に直結する具体的な活用シーンを説明していきます。
この記事を読むとわかること
- Flutter Navigatorの基本的な役割
- 画面遷移を行うための代表的な使い方
- 画面間でデータをやり取りする方法
- 実務で役立つ具体的な運用ポイント
- エラーや注意点を防ぐコツ
Flutter Navigatorとは何か
Flutter Navigatorとは、複数の画面(ScreenやPage)を積み重ねて管理し、ユーザーが戻る操作をしたときに前の画面へ戻る挙動をサポートする仕組みです。 スマートフォンアプリで「戻る」や「次へ進む」を自然に実装するための重要なクラス・ウィジェット群と考えてください。 具体的には、画面をスタック構造(後入れ先出し)で保持しており、新しい画面を表示する際はスタックの上に画面を積み、戻るときはスタックを一つポップするイメージです。 これにより、ユーザーがアプリをシームレスに操作できるようになります。
Navigatorのイメージ図
Navigatorをイメージしやすいように、簡単な模式図を考えてみましょう。
[画面A] (Bottom of stack)
↓ (push)
[画面B] (Top of stack)
最初に「画面A」だけが存在している状態で、Navigatorを利用して新しい「画面B」に遷移したとします。 すると、画面Bがスタックの一番上に積まれて表示されます。 ユーザーが戻る操作をすると、スタック上の画面Bが取り除かれ、下にある画面Aが再び表示されるわけです。
Flutter Navigatorの基本的な使い方
ここからは、Flutter Navigatorをどのように使えば複数画面の切り替えが実装できるのかを見ていきましょう。 初心者の方にもわかりやすいようにシンプルな例を示していきます。
pushとpop
最も基本的な関数は、push と pop です。 pushは現在の画面の上に、新しい画面を積み上げます。 popはスタックの上にある画面を取り除いて、1つ前の画面に戻る動作を行います。
以下は簡単なコード例です。画面Aから画面Bへ遷移し、戻る動作を確認できます。
import 'package:flutter/material.dart'; class FirstPage extends StatelessWidget { const FirstPage({Key? key}) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('画面A'), ), body: Center( child: ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => const SecondPage()), ); }, child: Text('画面Bへ進む'), ), ), ); } } class SecondPage extends StatelessWidget { const SecondPage({Key? key}) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('画面B'), ), body: Center( child: ElevatedButton( onPressed: () { Navigator.pop(context); }, child: Text('戻る'), ), ), ); } }
このように、pushで画面Bを表示し、popで画面Aに戻ります。 ボタンをタップするたびに画面が前後するのが確認できるでしょう。
画面間でデータを受け渡す
アプリでは「ある画面で入力した値を別の画面で表示する」といった場面がよくあります。 Navigatorでは、pushするときに引数を渡し、popで戻ってくる際に結果を受け取ることが可能です。 例えば、画面Bで何らかの入力を受け付け、その値を画面Aに返したい場合は以下のように実装できます。
// 画面A ElevatedButton( onPressed: () async { final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => const SecondPage()), ); // 受け取ったresultを処理する print('画面Bから受け取った値: $result'); }, child: Text('画面Bへ進む'), ); // 画面B ElevatedButton( onPressed: () { // 文字列など任意のオブジェクトを返せる Navigator.pop(context, '画面Bの入力結果'); }, child: Text('結果を返して戻る'), ),
この例では、画面Bから Navigator.pop(context, '画面Bの入力結果')
のように返すことで、await Navigator.push
側で結果を取得できます。
実務では、Formに入力した内容やユーザーが選択した値などを返すことが多いでしょう。
Named Routes
Flutterには、画面を切り替える際に文字列の名前を使って指定する仕組みも備わっています。 これをNamed Routes と呼びます。 MaterialAppのプロパティでルートを定義しておくと、pushNamedで画面を遷移できるようになります。
import 'package:flutter/material.dart'; void main() { runApp( MaterialApp( initialRoute: '/', routes: { '/': (context) => const HomePage(), '/second': (context) => const SecondPage(), }, ), ); } class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Home')), body: Center( child: ElevatedButton( onPressed: () { Navigator.pushNamed(context, '/second'); }, child: Text('SecondPageへ'), ), ), ); } } class SecondPage extends StatelessWidget { const SecondPage({Key? key}) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('SecondPage')), body: Center( child: Text('ここが画面Bです'), ), ); } }
あらかじめ、MaterialAppの routes
プロパティでルート名とウィジェットのマッピングを指定しておきます。
ここでは initialRoute: '/'
が画面Aに当たり、 '/second'
が画面Bという扱いです。
pushNamedやpopを使えば、同じように画面間遷移が行えます。
onGenerateRouteを活用する
アプリ内でルート設定がたくさん増えてくると、単純なmap形式の routes
では管理しにくくなるかもしれません。
そうした場合に活用できるのが、onGenerateRoute
というプロパティです。
ここでは、ルート名に応じて動的にページを生成し、MaterialPageRouteを返すような実装が可能です。
import 'package:flutter/material.dart'; void main() { runApp( MaterialApp( onGenerateRoute: (settings) { if (settings.name == '/second') { return MaterialPageRoute( builder: (context) => SecondPage(data: settings.arguments), ); } // デフォルトルート return MaterialPageRoute( builder: (context) => const HomePage(), ); }, ), ); } class HomePage extends StatelessWidget { const HomePage({Key? key}) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Home')), body: Center( child: ElevatedButton( onPressed: () { Navigator.pushNamed( context, '/second', arguments: '渡したいデータ', ); }, child: Text('SecondPageへ'), ), ), ); } } class SecondPage extends StatelessWidget { final Object? data; const SecondPage({Key? key, this.data}) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('SecondPage')), body: Center( child: Text('受け取ったデータ: $data'), ), ); } }
onGenerateRoute
の中で、ルート名に応じて MaterialPageRoute(builder: ...)
を切り替えているのがポイントです。
これにより、たとえば特定の条件下で別の画面を返すといった柔軟な処理が可能になります。
実務での活用シーン
Flutter Navigatorを使いこなすと、さまざまなシーンで役立ちます。 以下では、実務でよくある例をいくつかピックアップしてみましょう。
ログイン画面からメイン画面への遷移
ユーザーがログインしたあと、ホーム画面やダッシュボード画面へ一気に遷移させたいケースは多いでしょう。 ログイン時にユーザー情報やトークンを受け取り、popせずに別のルートへ移動してメイン画面に切り替えます。 そうすることで、戻るボタンを押してもログイン画面に戻らないように制御できます。
一覧画面から詳細画面への遷移
ECサイトなどでは、商品一覧から1つの商品を選択して詳細画面を表示する流れがよくあります。 詳細画面でレビューを表示したり、カートに追加したりといった操作を行い、戻るボタンで一覧画面へ戻るような実装をNavigatorでシンプルに行えます。
複数ウィザード形式の入力
ユーザー登録やステップ分けが必要なフォームなどでは、1画面ごとに必要な項目を入力させる場合があるでしょう。 Navigatorを使ってステップを区切れば、「次へ進む」ボタンで画面を切り替え、最後にpopで戻ることができます。
Navigatorを使う際の注意点
Navigatorは便利ですが、使い方を誤ると画面遷移が複雑になったり、戻り動作が意図しない形になったりすることがあります。 アプリが大規模になるほど、画面遷移の構造を整理しておくことが大切でしょう。
スタックが深くなりすぎるリスク
何度もpushを繰り返すと、ユーザーが「戻る」を押さない限り画面が積み重なり続けます。 あまりにも深いスタックになると、操作性が落ちるだけでなく、コードの可読性も下がります。 不要な画面はpopや置き換えなどを適切に行い、スムーズな遷移設計を心がけましょう。
GlobalKeyやNavigatorの複数管理に注意
Flutterでは、ルートごとにNavigatorを分ける構成も可能です。 例えば、TabBarと組み合わせてタブごとにNavigatorを設置し、それぞれスタック管理を行うアプリも存在します。 しかし、初心者のうちは全体を1つのNavigatorで管理したほうが混乱しにくいでしょう。 タブ内でさらにネストしたNavigatorを使うときは、GlobalKeyなどを使う場面があるため、慣れてきてから検討してみてください。
遷移先画面から戻れないケースに注意
Navigatorの挙動を制御していると、まれに「戻る」操作で本来戻るはずの画面が表示されないケースが発生することがあります。 特に、pushReplacementやpushAndRemoveUntilなどの関数を使って画面スタックを再構成する場合、意図せずスタックを空にしてしまうと戻る画面がなくなるでしょう。 こういった関数を使うときには、画面遷移のシナリオを明確にしておく必要があります。
pushReplacementやpushAndRemoveUntilをむやみに使うと、戻りの操作ができなくなることがあります。 大きな構造変更を行うときは、画面スタックがどうなるか事前にしっかり確認しましょう。
Navigatorと状態管理を組み合わせる考え方
画面遷移と状態管理を組み合わせると、より直感的で保守しやすい設計になります。 例えば、ProviderやRiverpodなどを使ってグローバルな状態を管理し、遷移先の画面でも同じ状態にアクセスする設計が考えられます。
このような設計を導入すると、ユーザー情報やアプリ全体に共有したいデータを1カ所で管理できるため、画面間のデータ受け渡しが軽減されるでしょう。 ただし、状態管理ツールを使うと実装が複雑に感じることがあるかもしれません。 まずはNavigator単体の仕組みをしっかり押さえてから導入を検討してみると良いですね。
画面トランジションのカスタマイズ
画面遷移時のアニメーション効果をカスタマイズする方法もあります。 通常はMaterialPageRouteを使うと、プラットフォームごとのデフォルトアニメーションが適用されますが、PageRouteBuilderを使えばフェードやスライドなどの演出を細かく設定できます。
例えば、下から画面がスライドしてくるアニメーションを取り入れることで、モーダルウィンドウのような見た目にすることも可能です。 ユーザーにとって見やすいトランジションを適宜選択すると、アプリ全体の操作感が向上するでしょう。
Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => SecondPage(), transitionsBuilder: (context, animation, secondaryAnimation, child) { final offsetAnimation = Tween<Offset>( begin: Offset(0.0, 1.0), end: Offset.zero, ).animate(animation); return SlideTransition( position: offsetAnimation, child: child, ); }, ), );
このように、PageRouteBuilderを使うと、transitionsBuilder
でアニメーション効果を自由に設計できます。
実務では、ユーザー体験を高めるためにカスタマイズするケースが少なくありません。
共同開発におけるNavigator管理
実務では、複数のエンジニアが同じプロジェクトを開発することも多いでしょう。 その際、Navigatorのルート設定や画面構成が散らばると、誰がどの画面をどのタイミングで呼び出しているのか把握しにくくなります。 チーム開発では以下のようなポイントを意識すると良いでしょう。
- ルート名やクラス名の命名規則を統一する
- 画面遷移フローをドキュメントや図などで可視化しておく
- pushReplacementやpushAndRemoveUntilを使う際のルールを決めておく
これらを守ることで、画面遷移が原因のバグや混乱を防ぎやすくなります。
命名規則を統一するだけで、チーム全体の認識がずれにくくなります。 たとえば「/home」「/login」「/detail/:id」のようにURL風に定義しておくとわかりやすいかもしれません。
実装を効率化するためのヒント
Navigatorの仕組み自体はシンプルですが、画面数が多くなると全体管理が煩雑になることもあります。 実務で複数画面を管理するときは、以下のような工夫をするのも一つの方法です。
- 画面を単一機能ごとにディレクトリで整理し、Navigator関連のコードもそこにまとめる
- 画面遷移を増やすときには、まずフローチャートを描いてから実装に移る
- 遷移の試作を短いスパンで行い、余計な画面やステップがないかを確認する
こういった下準備を行うことで、Navigatorの乱立を防ぎ、あとから見直したときにも理解しやすいプロジェクト構成を維持できるでしょう。
まとめ
Flutter Navigatorは、画面遷移を管理するうえで欠かせない存在です。 pushやpop、Named Routesなどの基本的な手法を理解するだけでも、複数画面のやりとりはスムーズにできるでしょう。 さらに、onGenerateRouteやカスタムアニメーションを駆使すると、大規模アプリでも柔軟に対応できます。
実務でのアプリ開発では、Navigatorの設計次第でユーザー体験にも大きな差が生まれます。 初心者の方はシンプルな画面構成から始め、段階的に画面数や状態管理を組み合わせてみてください。 少しずつ経験を積みながら、読みやすく保守しやすいコードを目指すことが大切ですね。