【Flutter】 Navigator から Go Router へ
初めに
Flutterを始めてから画面遷移に関してはずっとNavigatorを使ってきていたのですが、最近 Go Router の学習を始めました。
Navigator は初め記述方法を覚えるコストがあるものの、アプリ開発をしていれば幾度となく画面遷移の処理を書くことになるので、自然と書き方を覚えていきます。覚えてしまえば直感的に書けるので特に不便は感じていなかったのですが、これを機に Go Router と比較してみたいと思います。
Go Router に触れた後は Go Router Builder にも触れる予定なので、よろしければそちらも併せてご覧ください。
記事の対象者
- Flutter 学習者
- Go Routerを使いたい方
実装
導入
go_routerパッケージの最新バージョンを pubspec.yaml
に記述
dependencies:
flutter:
sdk: flutter
go_router: ^12.0.1
または
以下をターミナルで実行
flutter pub add go_router
ルートの指定
router.dart
という名前のファイルを作成して、その中に GoRoute の定義をまとめます。
import 'package:go_router/go_router.dart';
import 'package:go_router_builder_sample/screens/about_screen.dart';
import 'package:go_router_builder_sample/screens/home_screen.dart';
import 'package:go_router_builder_sample/screens/setting_screen.dart';
final router = GoRouter(
debugLogDiagnostics: true,
initialLocation: '/',
routes: [
GoRoute(
name: 'home',
path: '/',
builder: (context, state) => const HomeScreen()),
GoRoute(
name: 'about',
path: '/about',
builder: (context, state) => const AboutScreen()),
GoRoute(
name: 'setting',
path: '/setting',
builder: (context, state) => const SettingScreen()),
],
);
上のコードのように遷移させたいページのルートを指定し、それを router
変数としてグローバルに定義することで他のクラス内でも router
を使うことができます。
debugLogDiagnostics: true
debugLogDiagnostics: true,
とすることで以下のように、登録されているすべてのルートと遷移した際にどのルートに遷移したかがデバッグコンソールに表示されます。
[GoRouter] Full paths for routes:
=> /
=> /about
=> /setting
known full paths for route names:
home => /
about => /about
setting => /setting
[GoRouter] setting initial location null
[GoRouter] Using MaterialApp configuration
[GoRouter] going to /about
[GoRouter] going to /setting
[GoRouter] going to /
[GoRouter] going to /about
initialLocation: '/'
initialLocation: '/'
のように指定すると、指定されたパスにあるページが最初に表示されるようになります。例えば、initialLocation: '/about'
とすると AboutScreen
が初めに表示されるようになります。
なお、initialLocation を指定しなかった場合はパスが /
になっているものが自動的に一番初めに表示されるページになります。
上の HomeScreen
、 AboutScreen
、SettingScreen
は以下のように指定しておきます。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Home")),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/about');
},
child: const Text(
"Go To About Screen",
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class AboutScreen extends StatelessWidget {
const AboutScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('About'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/setting');
},
child: const Text(
'Go To Setting Screen',
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class SettingScreen extends StatelessWidget {
const SettingScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Setting')),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/');
},
child: const Text(
'Go To Home Screen',
),
),
),
);
}
}
各ページには次のページへ進むためのボタンが設けられており、HomeScreen
→ AboutScreen
→ SettingScreen
の順番に遷移するようになっています。
main.dart の変更
ルートの指定はできましたが、このままでは指定したルートを使うことはできません。
最後に main.dart
を以下のように変更することでルートを使えるようになります。
import 'package:flutter/material.dart';
import 'package:go_router_builder_sample/router.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp.router( // MaterialApp.routerなので注意
title: 'Go Router Sample',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
// 以下3行を追加
routerDelegate: router.routerDelegate,
routeInformationParser: router.routeInformationParser,
routeInformationProvider: router.routeInformationProvider,
);
}
}
Navigator.push
先ほどのコードでも示した通り、以下のコードが Navigator と go router を使用した場合に同じような動作になります。
Navigator
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AboutScreen(),
),
);
go router
context.go('/about');
以上のように context.go('遷移先のパス')
として遷移先のパスを引数に入れることで画面遷移を実装することができます。
ただ、go router で遷移した場合には以下のように「戻るボタン」が表示されないという違いがあります。これについては後述します。
Navigator.push | go_router |
---|---|
Navigator.pop
Navigator
Navigator.pop(context);
go router
context.pop();
Navigator.pop(context)
に関してはそのまま実行してもエラーは起きませんが、context.pop()
を実行すると以下のエラーが出力されます。
GoError (GoError: There is nothing to pop)
エラー内容としては書いてある通りで、popするものがないとのことです。
このエラーの原因は、popされる予定だったスクリーンはどのスクリーンの子要素、つまり入れ子ではないために起こっています。
ここで現在の router.dart
に以下のような DetailScreen
を追加してみましょう。
import 'package:go_router/go_router.dart';
import 'package:go_router_builder_sample/screens/about_screen.dart';
import 'package:go_router_builder_sample/screens/detail_screen.dart';
import 'package:go_router_builder_sample/screens/home_screen.dart';
import 'package:go_router_builder_sample/screens/setting_screen.dart';
final router = GoRouter(
debugLogDiagnostics: true,
initialLocation: '/',
routes: [
GoRoute(
name: 'home',
path: '/',
+ routes: [
+ GoRoute(
+ name: 'detail',
+ path: 'detail',
+ builder: (context, state) => const DetailScreen(),
+ )
+ ],
builder: (context, state) => const HomeScreen()),
GoRoute(
name: 'about',
path: '/about',
builder: (context, state) => const AboutScreen()),
GoRoute(
name: 'setting',
path: '/setting',
builder: (context, state) => const SettingScreen()),
],
);
GoRoute
の routes
にさらに GoRoute
を指定することで、そのルートの子要素、遷移先として新たなページのルートを指定することができます。
DetailScreen は以下のように AppBar のみの画面にしています。
import 'package:flutter/material.dart';
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Detail"),
),
);
}
}
以下のように HomeScreen
から DetailScreen
に遷移するパスを指定すれば下の画像のように「戻るボタン」を持つ DetailScreen
に遷移することができます。
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
static String get routeName => 'home';
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Home")),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/detail');
},
child: const Text(
"Go To Detail Screen",
),
),
),
);
}
}
HomeScreen
の入れ子としてルート指定された DetailScreen
Navigator.pushNamed
Navigator
Navigator.pushNamed(context, '/detail');
go router
context.goNamed('detail');
goNamed
でも階層で下にあるページに関しては「戻るボタン」がある状態で画面に遷移します。
Navigator.pushReplacement
Navigator
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(),
),
);
go router
context.pushReplacement('/detail');
pushReplacement
では現在のページを差し替える形で新たなページに遷移します。
この場合だとHomeScreen
を差し替える形でDetailScreen
に遷移します。
実際にpushReplacement
で遷移すると、これ以上戻るページが存在しないため、以下の画像のように戻るボタンがない状態で遷移します。
値渡し①
まずは DetailScreen
で userName
というString型の変数を受け取るように変更します。
import 'package:flutter/material.dart';
class DetailScreen extends StatelessWidget {
const DetailScreen({
super.key,
+ required this.userName,
});
+ final String userName;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Detail"),
),
+ body: Center(
+ child: Text(
+ "Hello $userName !",
+ ),
+ ),
);
}
}
Navigator
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(
userName: 'Koichi5',
),
),
);
go router
GoRoute(
name: 'detail',
path: 'detail',
builder: (context, state) => DetailScreen(
+ userName: state.extra as String,
),
),
context.go('/detail', extra: 'Koichi5');
go router では、router.dart
で DetailScreen
に渡す引数を state.extra で指定し、実際に遷移する処理では extra
プロパティに渡したい値を指定します。
両方とも以下のように遷移した先で受け取った値を使用することができます。
値渡し②
GoRoute(
name: 'detail',
path: 'detail/:user_name/:user_id',
builder: (context, state) {
final userName = state.pathParameters['user_name'];
final userId = state.pathParameters['user_id'];
return DetailScreen(
userName: userName!,
userId: int.parse(userId!),
);
},
),
import 'package:flutter/material.dart';
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key, required this.userName, required this.userId});
final String userName;
final int userId;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Detail"),
),
body: Center(
child: Text(
"Hello $userName ! \n Your ID is $userId.",
),
),
);
}
}
context.go('/detail/Koichi5/101');
以上のように、GoRouteのパスに:変数名
を入れることで値を渡すこともできます。
パスとして渡された変数を取り出すためには state.pathParameters['user_name']
のように state.pathParameters で一致する変数名を代入すれば取り出すことができます。
このような渡し方はユーザー固有の情報やリストの詳細情報などに使用されることが多いです。
本記事も https://zenn.dev/articles/50c553218ca938/edit
という記事になっており、articles
のIDとして50c553218ca938
が与えられていると考えることができ、 GoRouteのパスに指定する方法はこれと似ていると言えます。
実行すると以下のようにユーザー名とIDが正確に渡せていることがわかります。
まとめ
最後まで読んでいただいてありがとうございました。
今回は Go Router を使った画面遷移が Navigator ではどれに当たるかに焦点を当てて比較してきました。Go Router をタイプセーフで使う Go Router Builder についても学習するつもりなので、よろしければそちらもご覧ください。
参考
Discussion