FlutterのWidgetテストをちゃんとできるような設計にするお勉強
FlutterのテストにはUnit / Widget / Integrationの3つの段階があるが、外部データソースのアクセスを利用しないでDartのオブジェクト上だけでテストを行うのが、UnitとWidgetのテスト。
とはいっても、昨今で外部リソースを一切使わないケースはない。
ただHTTP通信やローカルストレージをMockにした所で、ほとんど使えない。それらを内部で呼び出している関数(ロジック)をそっくりMockに差し替えることができる必要がある。もちろん、プロダクトで動くソースコードに手を加えることなく。ビューに外部データソースに関するロジックを埋め込むとウィジェットテストを行えないのだ。
このようなケースに対してどう対応するかだが、まぁ簡単に言えば、ビューから呼び出されているビジネスロジックをMockで上書きするしかないのでははかろうか。そのための仕組みとして、またまたRiverpodの出番となる。RiverpodにはProviderをオーバーライドできる機能があるのだ。
このソースを見た時に、ほおおおおおそういうことねぇえええと思った。
class MockNewsViewModel extends Mock implements NewsViewModel {} void main() { final mockNewsViewModel = MockNewsViewModel(); when(mockNewsViewModel.fetchNews).thenAnswer((_) => Future.value()); when(() => mockNewsViewModel.news) .thenReturn(Result.success(data: dummyNews)); testWidgets('App widget test', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ newsViewModelProvider.overrideWithValue(mockNewsViewModel), ], child: const App(), ), ); });
ViewModelに該当するクラスをそっくりそのままMockに差し替える。これはmocktailパッケージで提供される。なんでこんなことができるのかさっぱりわからないのが辛いので、いつかコードを読まないといけない。リフレクションに近いなにかだと思う。
差し替えたViewModelクラスに対して、適当な値を突っ込む。thenReturn
とかthenAnswer
がそれ。Mockだから決め打ちで良い。非同期で呼びされるメソッドの場合は、thenAnswerを使うようだ。staticなメソッドのMockはできないらしいが、インスタンスメソッド内でstaticメソッドが呼びされている場合は、どうなんだろう。多分大丈夫だろきっと。
ListViewのテストでは、hogeが2行目に表示されていることを捕まえる必要があり、下記にあるように、何行目のアイテムを捕まえるコードを書かないといけない。
1つ困ったのが、SQLiteのMockテスト。今回、読み取り専用のマスタデータをsqlite形式で持ってる。検索パラメーターは2つある。この場合、決め打ちでthenAnswer
しちゃったら、ビューの表示確認にはなるけど、SQLの組み立てのテストにはならない。SQLを文字列で組み立てるようなことをすると、テスタビリティが損なわれるんだな。他のやり方を考えよう。最悪はユニットテストレベルで担保すればいいや。
FutureProviderを持つ親画面を、子画面から更新する
Riverpodネタは続く。
カート一覧→タップで数量更新→画面が戻った時にカートが更新・・・されない!データは書き換わっているが、UIが更新されていなかった。
一覧画面のデータFutureProvider.autoDispose
を使って、画面下部のタブに表示している。iPhoneはわからないけど、Androidのエミュレータで試したところ、親→子に遷移する時は、親のProviderはautoDisposeの対象にならないみたい。
このような場合は、強制的にFutureProvider
を更新する必要がある。このAPIを使う。
使い方はベリーイージー。グローバルに定義している対象のproviderを引数に入れるだけ。
ref.refresh(yourProvider);
オチとしては、ローカルにデータを書き込む系の場合、たいてい非同期になっている。なので、await
を入れて非同期の書き込み終了が完了してから(もしくはthen
のコールバックで)refresh
を呼び出さないとつまらない結果を生む。
RiverpodのProviderが管理するStateに引数を与えて初期化したい
画面遷移とかでよくあるよね。一覧から行タップで詳細に遷移する系のやつ。商品一覧→商品詳細みたいなやつです。
Riverpodが提供するProviderにはfamily
という関数があり、Providerを初期化する際に任意の型の引数を与えることができる。今回は、StateNotifierProviderでありながら、autoDispose
もfamily
も使うというこんもりセットだった。
// 第1引数: 利用するStateNotifierの型 // 第2引数: 第1引数に入れたStateNotifierで管理するStateの型 // 第3引数: 管理するStateの初期化に必要なパラメータの型 final calcProvider = AutoDisposeStateNotifierProviderFamily<CartPageStateNotifier, Cart, Cart>( (ref, param) => CartPageStateNotifier.init(cart: param)); //Widget内部ではこうなる。ConsumerWidgetを使っています。 //このitemは画面遷移でもらったデータで Widget hogeWidget(BuildContext context, ScopedReader watch) { final state = watch(calcProvider(item)); final vm = watch(calcProvider(item).notifier); }
class CartPageStateNotifier extends StateNotifier<Cart> { CartPageStateNotifier() : super(const Cart()); CartPageStateNotifier.init({required Cart cart}) : super(cart); }
こんな感じで、CartPageStateNotifier
に名前付きのコンストラクタを定義しておく。init
は渡したいデータでstate
を初期化する際に呼び出す。
RiverPodの設計思想は勉強になるなぁ.. というか、型がある言語はナイスだなぁ... PythonもTypeHintあるけどね。おほほンゴ。
RiverpodでinitStateを使わず、autoDisposeでUIを再構築する
FlutterのRiverpodの話。
10月に入って、作りかけのアプリのProvider -> Riverpodへのリプレイスを行っています。zennで公開されているRiverpod解説には大変お世話になりました。
ひとつわからなかったのが、「タブを切り替えた時に自動でそのページの中身をリロードして欲しい」という処理の書き方。
StatefulWidgetであれば、initState
が使える。ただ、Riverpodで提供されているHookWidgetやConsumerWidgetには、StatefulWidgetが提供するライフサイクルが存在しない。少なくとも、Riverpodの1.4系だと。
お買い物かごにアイテムを何点か追加して、お買い物かごタブをタップした時、タップに応じて画面を最新化する必要がある。カートはローカルにあるデータで、FutureProvier
を使って画面を構築していますが、タップでタブを切り替えてもUIが再構築されない。providerをキャッシュしているから当たり前だけど、ユーザーの画面操作でproviderを更新できないので、どうしようか悩んだ。
結論は単純だった。autoDispose
で自動廃棄してもらうだけ。
final cartController = FutureProvider.autoDispose<CartState>((ref) { return loadCart(); });
autoDisposeはスクリーンが消えると読み出してくれるようで、タブで画面を切り替えると、providerで保持した値も全部消える。裏返せば、タブを切り替えるタイミングで毎回データが初期化されるので、providerの値が常に更新され、UIが再構築されました。
最新情報を常に取ってこないといけない類の画面の実装は、FutureProvier.autoDispose
で行けそうだね。state
も任意のタイミングで更新できますので。ちゃんちゃん。
Android StudioでUnable to find bundled Java version
flutter doctor
でこの警告が出る場合。一定の手順に沿って行うだけ。JetBrain Toolboxを使っている人の対応策も書いてあった。
Macのユーザー名とAndroidStudioのバージョンは、その時の最新に差し替えれば良さそう。
cd /Users/<USER_NAME>/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/<APP_VERSION>/Android Studio.app/Contents/jre ln -s ../jre jdk ln -s "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin" jdk flutter doctor -v
FileSystemObjectがネットワークドライブ上のファイル探索でクソ遅い件
ネットワークドライブ上の35個のExcelファイルのOPEN→転記に30分近く要していた。
おせえええええええ、死ねええええええ。
理由はわからないけど、FileSystemObjectを使ってネットワークドライブ上のファイル走査すると、クソ遅い。なので、Dir関数を使う方法に変更したら、6分に短縮された。
理由はわからない。