LoginSignup
20
14

More than 3 years have passed since last update.

Flutter/Navigator遷移にBuildContextを不要にする/遷移のWidgetTest

Last updated at Posted at 2020-03-30

環境

訳あって最新版を使っていません(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に追加します。

pubspec.yaml
dependencies:
  ....
  get_it: ^4.0.1

package getしておいて下さいね。

(5) NavigationServiceを実装して使う

もうあとは上記のサイトのステップ通りですが、一応コードを載せていきます。

a.NavigationServiceを実装する

普通にクラスを作ります。

NavigationService.dart
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に遷移関数を作る

MainViewModel.dart
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/

20
14
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
14