Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

DockerのSelenium-HQで「cannot create temp dir for user data dir in chrome driver」

DockerのSeleniumで、こんなエラーが出ました。

Unknown error: cannot create temp dir for user data dir in chrome driver

MacのDockerのディスクが32GBで、30GB使っていた。2GBあったらええんちゃうかと思ってたけど、足りないようで... MacのDockerのディスク容量を増やしたら解決しました。おしまい。

FlutterのWidgetテストをちゃんとできるような設計にするお勉強

FlutterのテストにはUnit / Widget / Integrationの3つの段階があるが、外部データソースのアクセスを利用しないでDartのオブジェクト上だけでテストを行うのが、UnitとWidgetのテスト。

とはいっても、昨今で外部リソースを一切使わないケースはない。

  • HTTPSAPI通信
  • FirebaseのRemoteConfig
  • SharedPrefrenceに代表されるOSが持つローカルストレージ
  • SQLiteのようなローカルDB

ただHTTP通信やローカルストレージをMockにした所で、ほとんど使えない。それらを内部で呼び出している関数(ロジック)をそっくりMockに差し替えることができる必要がある。もちろん、プロダクトで動くソースコードに手を加えることなく。ビューに外部データソースに関するロジックを埋め込むとウィジェットテストを行えないのだ。

このようなケースに対してどう対応するかだが、まぁ簡単に言えば、ビューから呼び出されているビジネスロジックをMockで上書きするしかないのでははかろうか。そのための仕組みとして、またまたRiverpodの出番となる。RiverpodにはProviderをオーバーライドできる機能があるのだ。

このソースを見た時に、ほおおおおおそういうことねぇえええと思った。

github.com

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行目に表示されていることを捕まえる必要があり、下記にあるように、何行目のアイテムを捕まえるコードを書かないといけない。

stackoverflow.com

1つ困ったのが、SQLiteのMockテスト。今回、読み取り専用のマスタデータをsqlite形式で持ってる。検索パラメーターは2つある。この場合、決め打ちでthenAnswerしちゃったら、ビューの表示確認にはなるけど、SQLの組み立てのテストにはならない。SQLを文字列で組み立てるようなことをすると、テスタビリティが損なわれるんだな。他のやり方を考えよう。最悪はユニットテストレベルで担保すればいいや。

FutureProviderを持つ親画面を、子画面から更新する

Riverpodネタは続く。

カート一覧→タップで数量更新→画面が戻った時にカートが更新・・・されない!データは書き換わっているが、UIが更新されていなかった。

一覧画面のデータFutureProvider.autoDisposeを使って、画面下部のタブに表示している。iPhoneはわからないけど、Androidエミュレータで試したところ、親→子に遷移する時は、親のProviderはautoDisposeの対象にならないみたい。

このような場合は、強制的にFutureProviderを更新する必要がある。このAPIを使う。

pub.dev

使い方はベリーイージー。グローバルに定義している対象のproviderを引数に入れるだけ。

ref.refresh(yourProvider);

オチとしては、ローカルにデータを書き込む系の場合、たいてい非同期になっている。なので、awaitを入れて非同期の書き込み終了が完了してから(もしくはthenのコールバックで)refreshを呼び出さないとつまらない結果を生む。

RiverpodのProviderが管理するStateに引数を与えて初期化したい

画面遷移とかでよくあるよね。一覧から行タップで詳細に遷移する系のやつ。商品一覧→商品詳細みたいなやつです。

Riverpodが提供するProviderにはfamilyという関数があり、Providerを初期化する際に任意の型の引数を与えることができる。今回は、StateNotifierProviderでありながら、autoDisposefamilyも使うというこんもりセットだった。

// 第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のバージョンは、その時の最新に差し替えれば良さそう。

stackoverflow.com

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分に短縮された。

理由はわからない。