[Flutter][Riverpod] .autoDispose使用時のunit testにおける注意点: listen()を使用する

公開:2021/01/26
更新:2021/02/23
2 min読了の目安(約1800字TECH技術記事

はじめに

.autoDisposeを使用すると、例えば画面遷移時にWidgetのインスタンスと共にそのWidgetに紐づくクラス(例:ViewModel)のインスタンスも同時に破棄でき、再度画面を開いた時に初期状態で表示するということが可能になります。AndroidのViewModelに近い挙動です。

注意点

.autoDisposeの動作タイミングは、状態を購読するオブジェクトが存在しなくなった時です。これにより状態を購読しているWidgetが破棄されると、その状態(eg: ViewModel)も破棄されます。

つまりtestにおいても明示的にlistenerを登録しておく必要があります。登録しない場合は即座に対象クラスがdisposeされてしまいテストが失敗します。

ChangeNotifierを継承したViewModelをunit testする例で考えます。このViewModelが以下のような形で.autoDisposeを使用してDIされるとします。

final viewModelProvider = ChangeNotifierProvider.autoDispose(
    (ref) => viewModel(repository: ref.read(repositoryProvider)));

例: ProviderContainer.readでは購読にならないため即座にdisposeされる

test('ViewModel test', () async {
    final repository = MockRepository();
    when(repository.getData(any)).thenAnswer((_) async => data);
    
    final container = ProviderContainer(
      overrides: [repositoryProvider.overrideWithValue(repository)],
    );
    
    final viewModel = container.read(viewModelProvider); // 👈
    await viewModel.fetchData();
    
    expect(viewModel.data, data);
  });

以下のようなエラーとなりテストが失敗するはずです。

A ViewModel was used after being disposed.
  Once you have called dispose() on a ViewModel, it can no longer be used.

ただし、.autoDisposeを使用していない場合は上記でも動作します。

解決策

以下のようにProviderContainer.listenを呼び、そこからViewModelを取得してテストを行います。
これによりテスト終了までViewModelを破棄されることなく動作させることができます。

test('ViewModel test', () async {
    final repository = MockRepository();
    when(repository.getData(any)).thenAnswer((_) async => data);
    
    final container = ProviderContainer(
      overrides: [repositoryProvider.overrideWithValue(repository)],
    );

    final viewModel = container.listen(viewModelProvider).read(); // 👈
    await viewModel.fetchData();
    
    expect(viewModel.data, data);
  });

以上です!