所感箱

所感を書きためていきます

MVVM構成でStreamを扱う時に試したこと

Flutterの勉強をかねてflutter-architecture-blueprintsを参考にアプリの実装を進めているが、Streamの扱いをMVVMでどう実装すると良いのか迷ったので、書いておく。

github.com

MVVMへのStreamの追加

先述のレポジトリではMVVMの状態管理とDIにriverpodを利用している。 それとflutter_hooksChangeNotifierを組み合わせてMVVMを実装している。 ※詳しくは下記で本人が解説してくれている記事を参照よければそちらを・・・!。

wasabeef.medium.com

この構成を参考は下記のような実装を試していた。

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に伝わる。

  1. RepositoryでFirestoreのデータ変更時にStreamを流れる
  2. ViewModelでRepositoryを講読しgoodsに、notifyListeners()でViewに通知
  3. ViewModelのgoodsを監視して、変更があった場合にリビルドする
  4. 不要になった場合に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()
  );
});

このアプローチだと状態の変更は以下の流れになった。

  1. goodsStreamProviderでデータに変更時にStreamを流す
  2. ViewModelのgoodsでgoodsStreamProvidernのStreamをもつ
  3. ViewModelのgoodsを監視して、変更があった場合にリビルドする
  4. 不要になると自動で開放される

このアプローチだとStreamの開放もれを防ぐことは実現できたように思う。 反面、Providerがデータの変更を直接管理するため、Repositoryクラスにデータ取得ロジックの集約がしづらくなる面もありそうだと感じた。

引き続き良い方法を探していきたい。