環境
訳あって最新版を使っていません(macをCatalinaにアップデートできない==Xcodeの最新版を入れられないので)。
従って、バージョン違いによる不動作などあるかも知れません。お気づきの点があったら是非コメント下さい。
$ flutter --version
Flutter 1.12.13+hotfix.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 27321ebbad (4 months ago) • 2019-12-10 18:15:01 -0800
Engine • revision 2994f7e1e6
Tools • Dart 2.7.0
1. Navigatorで遷移するときにBuildContextを不要にしたい
(1)通常の書き方
通常、Naviatorでの画面遷移はこのように書くと思います。
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => NextPage(),
),
);
あるいは、MaterialAppでこんな風に指定しておいて、
return MaterialApp(
// Navigator
routes: {
'/': (context) => HomePage(),
'/next': (context) => NextPage(),
},
initialRoute: "/",
遷移時には、こう書くことも出来ます。
Navigator.pushNamed(context, '/next');
基本的にはこれで事足ります。
(2)問題点
ソースコードを見返しているときに、次のことに気付きました。
- 「遷移のコードどこ行った?」
ページのウィジェット構成が深いネストになっていて、かつ、子ウィジェットをクラス化して作っていて、さらにそれがページのルートのコードがあるファイルとは分かれている。
そんな状況で、遷移のコードを探してファイルをウロウロすることが、よくあったのです。
検索掛ければ良いのでしょうが、Navigator
と打つのも面倒です。
なんとかわかりやすい場所に置けないかな?と思ったのです。
そこで、
- 「ViewModelクラスを作っているから、それを介して遷移させられないか?」
と考えました。
こんな形です。
class SubModule extends StatelessWidget{
...
onTap: () => viewModel.navigateNextPage();
}
class MainViewModel with ChangeNotifier{
void navigateNextPage(){
// 遷移の処理
}
}
(3)BuildContextが邪魔!
しかし、遷移処理(push)呼び出し時に、BuildContext
が必要です。
ViewModel
クラスにContext
を持たせる/渡すのは、恐らくMVVM的にアンチパターンです。
あ、申し遅れましたが、こちらを参考に、AndroidのAAC的なMVVMパターンでアプリを設計しています(いまさら)。
- 「BuildContextが要らないNavigatorの使い方ないかなあ」
ググっていたら、見つけました。そのままのタイトルです。
Navigate Without BuildContext in Flutter using a Navigation Service
https://www.filledstacks.com/post/navigate-without-build-context-in-flutter-using-a-navigation-service/
もう、嬉しいので、このままの手法で行くことにします。
(4)ライブラリを追加
get_itライブラリを使うようなので、pubspec.yaml
に追加します。
dependencies:
....
get_it: ^4.0.1
package getしておいて下さいね。
(5) NavigationServiceを実装して使う
もうあとは上記のサイトのステップ通りですが、一応コードを載せていきます。
a.NavigationServiceを実装する
普通にクラスを作ります。
import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState.pushNamed(routeName);
}
bool goBack() {
return navigatorKey.currentState.pop();
}
}
b.locatorを宣言してsetupメソッドを作る
場所はどこでもいいと思います。参考サイトではlocator.dart
を作っていますが、私は面倒なのでmain.dart
に含めてしまいました。
import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;
void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
}
c.setupLocatorをmain関数で呼び出す
最初にプロジェクトを作ったら、恐らく以降忘れ去られているであろう、main
関数を探します(笑)
void main() {
setupLocator();
runApp(MyApp());
}
d.ViewModelに遷移関数を作る
void navigateNextPage() {
final NavigationService _navigationService = locator<NavigationService>();
_navigationService.navigateTo('/input');
}
_navigationService
は、参考サイトではメンバにしていましたが、何となくローカル変数にしました。
e.ウィジェットのコードを変更する
プロパティkey
を使っていますが、これはテストでウィジェットを探せるようにするためです。探しやすいウィジェットならこれは不要ですが、あっても害はないはず。
今回、GridViewの中のセル用のウィジェットを探すため、index
をKeyにしました。
InkWell(
key: Key(index.toString()),
onTap: () => viewModel.navigateNextPage(),
child: ...
),
f.MaterialAppのルート設定をする
こちらの書き方になっていない場合は変更します。この指定方法じゃ無いとNavigatorServiceが動きません。
追記: navigatorKey
の追加も忘れないで下さい。
return MaterialApp(
// Navigator
navigatorKey: locator<NavigationService>().navigatorKey,
routes: {
'/': (context) => HomePage(),
'/next': (context) => NextPage(),
},
initialRoute: "/",
home
指定は消して下さい。
ここまでやれば、起動して動作確認が出来ます。
これで、
- どんなページがあるか一覧で見やすくなる
-
MaterialApp
のルート設定により
-
- 遷移のコードが見つけやすくなる
- ViewModelクラスに遷移そのもののコードがあることにより
と、色々探しやすくするという目的が達成できました。
ページの数が増えて、ファイル数が増えて、さらに多人数で開発することになったりしたら、こういう風に決まっているとみんなやりやすいんじゃないかな。
2. Navigator遷移のWidgetTest
さて、ついでなのでテストも書きます。
画面を実際には表示しないWidgetTestで出来ます。
testWidgets('セルのタップで次画面に遷移', (WidgetTester tester) async {
setupLocator();
await binding.setSurfaceSize(Size(1080, 1776));
// アプリ全体を起動(Navigator設定が必要なため)
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle(); // ローディング終了を待つ
await tester.tap(find.byKey(Key('10')));
await tester.pumpAndSettle(); // 遷移を待つ
expect(find.byType(NextPage), findsOneWidget);
expect(find.text('次のページタイトル'), findsOneWidget);
});
特に難しいことは無いですね。pumpAndSettle
を駆使すれば良いだけです。
後は、ウィジェットを探すのに、byKey
を使っているのが、ちょっと特殊です。
普通のボタンとかなら、もっと探しやすいと思いますが、ボタンが複数あるような画面だと、やはりkey
プロパティを付けるのがやりやすいんじゃないでしょうか。
あ、言い忘れてましたが、実機動作確認はAndroidのみで行っています。
古いiPadはあるんだけど、もうOSアップデートの対象外なんですよね。
iPhone格安で手に入らないかな・・・SIM無しでいいから・・・
参考
Testing tidbit - Writing widget tests for navigation events
https://iiro.dev/2018/08/22/writing-widget-tests-for-navigation-events/