RiverpodではなくBLoCという選択
既存アプリをFlutterでリプレースするにあたり、状態管理は最終的にBLoCを採用した。
小規模アプリではRiverpodも使ってきたが、自分の手元と当時のチーム状況ではBLoCが扱いやすいと判断した。以下に、そのときの判断根拠を記載する。
1. Providerの種類が多く迷いが増える
Provider / StateProvider / FutureProvider / StreamProvider / NotifierProvider / AsyncNotifierProvider ...- どれを選ぶかの判断がレビューでぶれがち
-
BLoCは
Bloc<Event, State>で一本化でき、学習・設計・レビューが揃えやすい
2. AsyncNotifierのAsyncLoadingで前データ保持が難しい
- 「初期表示のローディング」と「初期表示後の追加読み込み」を分けたい
- 初期表示:スピナーのみ
- 追加読み込み:前回データを表示し続けながらローディング
-
AsyncLoadingに切り替わると前データ保持や「初期/追読」の判別が重くなりがち -
BLoCなら
InitialLoading / Appending / Loaded([...])のように状態を素直に分離できる
3. AsyncValue(Loading/Data/Error)がsealedではない
-
switchで網羅性チェックを効かせたい -
BLoCは
sealedなStateにでき、switchの漏れをコンパイル時に検出できて安心
例(概念)
sealed class FeedState {} final class InitialLoading extends FeedState {} final class Appending extends FeedState { final List<Item> prev; Appending(this.prev); } final class Loaded extends FeedState { final List<Item> data; Loaded(this.data); } final class Failure extends FeedState { final Object error; Failure(this.error); } switch (state) { case InitialLoading(): ... case Appending(prev: final p): ... case Loaded(data: final d): ... case Failure(error: final e): ... } // defaultなしで漏れ検知(Linterで`no_default_cases`を有効にすると更に固い)
4. AsyncNotifier系の抽象化と寿命管理が重い
-
AsyncNotifierとAutoDisposeAsyncNotifierが別系統で、寿命戦略の違いが型に出る。 - 抽象/DI/テストダブルが二重化しやすく、土台の再利用性が落ちやすい。
-
BLoCは
Bloc<Event, State>に集約、破棄はclose()、スコープはBlocProviderで統一できる。
まとめ(当時の見立て)
| 観点 | Riverpod(見送り要因) | BLoC(採用要因) |
|---|---|---|
| API表面 | Providerが多く判断が割れる |
Bloc<Event, State>で一本化 |
| 非同期・読み込み |
AsyncLoadingで前データ維持が重い |
状態分割で保持/追記が明瞭 |
| 網羅性 |
AsyncValueが非sealedで不安 |
sealed state + switchで担保 |
| 抽象/寿命 |
AsyncNotifier/AutoDispose*二系統 |
破棄/スコープ/抽象が一貫 |
Riverpodが弱いという話ではなく、当時の要件と体制ではBLoCのほうが運用しやすかった——という結論。
付記:BLoCのボイラープレートは自動生成で薄める
BLoCは素で書くと手数が増えるため、bloc/state/eventの3ファイルは自動生成で吸収している。
方針:明快さはBLoCで取り、記述量は生成で相殺。
将来の要件やチーム構成次第では、選択は柔軟に見直す。