MVVM構成でStreamを扱う時に試したこと
Flutterの勉強をかねてflutter-architecture-blueprintsを参考にアプリの実装を進めているが、Streamの扱いをMVVMでどう実装すると良いのか迷ったので、書いておく。
MVVMへのStreamの追加
先述のレポジトリではMVVMの状態管理とDIにriverpodを利用している。 それとflutter_hooksやChangeNotifierを組み合わせてMVVMを実装している。 ※詳しくは下記で本人が解説してくれている記事を参照よければそちらを・・・!。
この構成を参考は下記のような実装を試していた。
View
// useProviderで取得。変更時にリビルドされる。 final goods = useProvider(goodsViewModelProvider.select((value) => value.goods)); useMemoized(() { goodsViewModel.subscribeGoods(); });
ViewModel
// ProviderからViewModelを生成 final goodsViewModelProvider = ChangeNotifierProvider((ref) => GoodsViewModel(ref.read(goodsRepositoryProvider))); class GoodsViewModel extends ChangeNotifier { GoodsViewModel(this._repository); final GoodsRepository _repository; List<Goods> goods; StreamSubscription disposable; void subscribeGoods() { disposable = _repository.getGoods() .listen((value) { goods = value; notifyListeners(); }); } @override void dispose() { super.dispose(); // 不要になったらStreamを開放する disposable.cancel(); } }
Repository
class GoodsRepositoryImpl implements GoodsRepository { //変更を即時反映するためにStreamを返す @override Stream<List<Goods>> getGoods() { CollectionReference ref = FirebaseFirestore.instance .collection("goods"); return ref.snapshots().map((snapshot) => snapshot.docs .map((docs) => docs.data()) .map((data) => Goods(data['id'], data['name']) ).toList() ); } }
この設計だと状態の変更は以下の流れでViewに伝わる。
- RepositoryでFirestoreのデータ変更時にStreamを流れる
- ViewModelでRepositoryを講読しgoodsに、notifyListeners()でViewに通知
- ViewModelのgoodsを監視して、変更があった場合にリビルドする
- 不要になった場合にViewModelのdisposeで開放をする
しかし、ViewModelでStreamをdisposeする方法では、コードの追加漏れで開放されないミスをしそうに感じていた。
StreamProvierを利用したアプローチ
調べてみたところ開放ミスの防止はStreamProviderとaudoDisposeでシンプルに実現できそうだったので修正を加えてみた。
View
final goodsStream = useProvider(goodsViewModelProvider(firebaseUser.uid).select((value) => value.goods)); // goodsのストリームを講読する final goods = useStream(useMemoized(() => goodsStream, [goodsStream.last.toString()]), initialData: []); if (goods != null && goods.data.isNotEmpty) { return RefreshIndicator( onRefresh: () async => {}, child: ListView.builder( itemCount: goods.data.length, padding: const EdgeInsets.only(top: 10.0), itemBuilder: (context, index) => _buildListItem(context, goods.data[index]), ), ); } else { return Text('Error Screen: '); } },
ViewModel
// Firestoreのデータに変更があった場にStreamが流れるようにwatchする final goodsViewModelProvider = AutoDisposeProvider( (ref) => GoodsViewModel(ref.watch(goodsStreamProvider.stream)) ); class GoodsViewModel extends ChangeNotifier { GoodsViewModel(this.goods); final Stream<List<Goods>> goods; }
Repository -> Provider
// AutoDisposeStreamProviderを使うと使われなると自動的に破棄される final goodsStreamProvider = AutoDisposeStreamProvider((ref) { CollectionReference ref = FirebaseFirestore.instance .collection("goods") return ref.snapshots().map((snapshot) => snapshot.docs .map((docs) => docs.data()) .map((data) => Goods(data['id'], data['name'], data['date'], data['user']) ).toList() ); });
このアプローチだと状態の変更は以下の流れになった。
- goodsStreamProviderでデータに変更時にStreamを流す
- ViewModelのgoodsでgoodsStreamProvidernのStreamをもつ
- ViewModelのgoodsを監視して、変更があった場合にリビルドする
- 不要になると自動で開放される
このアプローチだとStreamの開放もれを防ぐことは実現できたように思う。 反面、Providerがデータの変更を直接管理するため、Repositoryクラスにデータ取得ロジックの集約がしづらくなる面もありそうだと感じた。
引き続き良い方法を探していきたい。