1

Flutterでいい感じに状態管理
RIVERPODを採⽤するに⾄った理由

2

⾃⼰紹介
u

某クラウドベンダーでプロトタイピングuu

エンゲージメントに従事

A社のクラウドを使う時にブロッカーをクリアして導⼊までの時間短縮

Comictribes.comソフトウェア開発グループを主宰
uメインプロダクトはコミケカタログブラウザ

uAndroid、iOSのモバイルには以前から関⼼ありu

最近すごく興味を持っているのはMSX0

Agendaアプリのテクノロジー負債
思わず⽬移りする技術たち
⾃分の中のニーズを⾒つめ直す
状態保持で何を保持するか︖
何を採⽤することにしたか︖
フレームワークの⽐較
フレームワークの選定
Riverpodでできること
具体的な使い⽅CQRSとイベントソーシング
ListViewでCQRSを実装してみる
デバッグに便利なテクニック
最後に3

アプリのテクノロジー負債(重いよね・・・)
Comictribesの場合︓開発開始は2010年
そもそも⾔語が古い
iOSはobjective-c、AndroidはJava構造がそもそもレガシー
Fragmentベースのコード
画⾯更新系はAsyncTaskに依存
美味しそうなスパゲッティ
コードと画⾯更新系の全てをFat Fragmentで記述している4

思わず⽬移りする技術たちu

基本アーキテクチャuu

MVC,MVVM,DDD,Clean-Architecture etc。。。

フレームワークuFlutter、React Native、.Net系技術(Xamarin、MAUI)u

状態管理(Flutterだけでも結構ある）

u

⾮同期フレームワーク

uStream中⼼でデザインするか

5

6

どれを採⽤したらいいか迷ってしまう・・・・⾃分の中のニーズを⾒つめ直す
技術に⽬移りするよりも、何が重荷になっていたかを確認、優先度をつけてみる
uiOSとAndroidの両対応uuu

実質別アプリをメンテしてる感じ

アプリケーションデータの状態保持とライフサイクル管理u

最新かつメンテされてる状態管理を使いたいu

UIのニーズに応えられる状態管理を使いたい

ロジックと画⾯更新の疎結合
uTab画⾯を跨いだフローティングウィンドウを持つアーキテクチャを作れる

u

⾮同期のDB処理、API処理のエラー発⽣時の更新挙動をシンプルに実装したいu

画⾯更新は最低限にしたい。ListViewとかGridViewとか・・・

u

データ更新での画⾯更新を簡潔に記述したい

7

状態保持で何を保持するか︖
u

アプリのデータの保持に使うuuu

グローバルな共通API、レポジトリのエントリーポイントuDBアクセスやSharedPreferenceなどのレポジトリのハンドラを保持uインターネット上のRest APIを利⽤する際のLimitコントロール

u

更新を通知して表⽰データを更新するトリガー

グローバルな共通データ保持u

起動時の⽣成データの保持u

画⾯を跨いだ情報の保持、更新の通知

ウィジェットの表⽰データ保持u

できるだけウィジェットツリーの枝でデータを保持して更新を減らしたいu

ListViewなどで、個々のアイテムの情報保持と更新対象のみの部分更新

8

何を採⽤することにしたか︖
uFlutterの採⽤uuuMaterial DesignベースでUXを統⼀したいと考えていたのでこれは早い段階で決定

基本アーキテクチャはあえて決めないu

やりたいことは関⼼の分離、DIしやすいものを選ぶ

u

作りやすいことを重視

u

状態保持フレームワークの設計思想重視

状態管理u

継続性を重視し、できるだけ⼈気のフレームワークを使⽤u

平易な書き⽅ができるかuuRiverpodを採⽤ロジックと画⾯更新の疎結合u

画⾯更新は状態変更した時に発⽣するので、状態保持に依存

u

CQRSを実現するイベントソーシングを実装
uRiverpod (Notifier Providerを使⽤）

9フレームワークの⽐較（⼈気重視）
更新⽇時

10

Star

Like

Flutter Fovorite

URL

Provider2023/11/10 4900

9121

○

https://github.com/rrousselGit/provider

Riverpod

2023/10/31 5200

2800

○

https://github.com/rrousselGit/riverpod

bloc2023/11/7

1100

6123

○

https://github.com/felangel/bloc

RxDart2023/10/26 3300

2328

○

https://github.com/ReactiveX/rxdart

mobx

2023/10/31 2300

1100○

https://github.com/mobxjs/mobx.dart

getX

2023/11/3

9300

13168 ×

GetIt

2023/9/25

12003497×https://github.com/jonataslaw/getx
https://github.com/fluttercommunity/get_itフレームワークの選定（平易さ）11

概要
ProviderInheritedWidgetをより使いやすく、より再利⽤しやすくするためのラッパー

Riverpod

Providerと同⼀作者。ビジネスロジックの記述と、⾮同期処理に使⽤できるリアクティブ キャッシュ フ
レームワーク。状態保持にも利⽤可能

bloc

Blocアーキテクチャの考え⽅に基づいてStreamベースのデータ処理とCubitによる状態更新のemitをサポートするフレームワーク。Providerに依存

RxDart

いわゆるReactiveXのDartでの実装。Streamを拡張してさまざまなストリーム処理ができる。Blocライブ
ラリのような処理を⾃前で作り込む場合に使う。

mobx

MobXは、アプリケーションのリアクティブ・データとUI（または任意のオブザーバー）を簡単に接続できるステート管理ライブラリ。アノテーションを積極的に利⽤。Providerに依存できるだけ平易な書き⽅を志向したい（Stream処理は複雑な書き⽅ができてしまう）
アノテーションが多いのは、学習コストが多く望ましくない＝＞Riverpodを選定

12

Riverpodでできること
u

画⾯表⽰情報などのステート情報を保存、取得、更新できる

u

ステートを更新するとUIがリビルドされるu

ウィジェットの⼀部だけ更新することもできるu

ListItemなどで便利な引数付きのProviderやautoDisposeが利⽤できる
WidgetWidgetWidgetstate

WidgetProviderProviderstate具体的な使い⽅(non annotation)
import 'package:flutter/material.dart';
import'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod/riverpod.dart’;13

class MyApp extends StatelessWidget {@override
Widget build(BuildContext context) {importreturn Consumer('package:riverpod_annotation/riverpod_annotation.dart';builder: (context, ref, _) {final helloWorld = ref.watch(helloWorldProvider);part 'main.g.dart';
return MaterialApp(
監視設定︓helloWorldを監視
home: Scaffold(body: Center(
final helloWorldProvider = Provider((ref) => 'Hello World');
child: Text(helloWorld),), helloWorldを監視して変更したらビルド
定義︓ProviderをGlobal
scopeで関数定義
void main(){));
runApp(});
ProviderScope(}}child: MyApp(),));}

スコープ有効化︓Providerの検索スコープ設定

Riverpodの使い⽅（ non annotation ）14

Widgetと⼀緒に使う場合
u

u

定義⽅法
u

グローバルで定義されたprovider変数で定義

u.familyで引数付きのproviderや、.autoDisposeでWidgetの削除に同期して削除する挙動を選べる

利⽤可能な場所uu

ConsumerWidgetやWidgetツリーの中のConsumerから利⽤

利⽤⽅法
uRiverpodのウィジェットにBuildメソッドの引数に含まれるWidgetRefとproviderインスタンスから状態取得。
状態の取得には、呼び出した時点の状態を取得するProvider.readと動的な状態変化に追従するwatchの２種類がある。ほとんどのケースでwatchで問題ない。

uWidgetで状態更新をobserveしたい場合はステートをwatchで状態取得
XxxState state=ref.watch(xxxProvider);uStateNotifier等を継承して状態を保持している場合、クラス内のステート更新関数にアクセスしたい場合がある。
StateNotifier stateNotifier=ref.watch(xxxProvider.Notifier);でインスタンスを取得できる具体的な使い⽅（最近の書き⽅）

15

import 'package:flutter/material.dart';@override
import 'package:flutter_riverpod/flutter_riverpod.dart'; Widget build(BuildContext context) {importreturn Consumer('package:riverpod_annotation/riverpod_annotation.dart'
builder: (context, ref, _) {;final helloWorld = ref.watch(helloWorldProvider);import 'package:riverpod/riverpod.dart';
return MaterialApp( 監視設定︓helloWorldを監視
home: Scaffold(@riverpod
body: Center(
final String helloWorld = ((ref) => 'Hello World');
child: Text(helloWorld),),定義︓ProviderをGlobal scopeで関数定義
void main() {));
helloWorldを監視して変更したらビルドが⾛る
runApp(});
ProviderScope(}}child: MyApp(),));}

スコープ有効化︓Providerの検索スコープ設定

class MyApp extends StatelessWidget {Riverpodの使い⽅(2.０以降)

16

Widgetと⼀緒に使う場合
uu

定義⽅法
u

グローバルでprovider変数を⽣成して定義

u.familyで引数付きのproviderや、.autoDisposeでWidgetの削除に同期して削除する挙動を選べるu@riverpodアノテーションで{クラス名}providerが⾃動⽣成され、buildの引数で.familyを⾃動判定

uNotifierの.familyはアノテーションで作る前提となる

利⽤可能な場所uu

ConsumerWidgetやWidgetツリーの中のConsumerから利⽤

利⽤⽅法
uRiverpodのウィジェットにBuildメソッドの引数に含まれるWidgetRefとproviderインスタンスから状態取得。状態の取得には、呼び出した時点の状態を取得するProvider.readと動的な状態を取得するwatchの２種類がある。ほとんどのケースでwatchで問題ない。

uWidgetで状態更新をobserveしたい場合はステートをwatchで状態取得
XxxState state=ref.watch(xxxProvider);uStateNotifier等を継承して状態を保持している場合、クラス内のステート更新関数にアクセスしたい場合がある。StateNotifier
stateNotifier=ref.watch(xxxProvider.Notifier);でインスタンスを取得できる

17

CQRSとイベントソーシングuu

CQRS（コマンドクエリ責務分離）uCommands システムの状態を変更するが、値を返す必要がない

uQueries

結果を返すが、システムの状態の変更は⾏わない（副作⽤を伴わない）

イベントソーシングu

あるドメインで発⽣したイベントを他のドメインに供給するデザインパターン

Write
結果を待たず終了

コマンドonTap

UIEvent

Controller

状態変更
Read

状態変更
クエリ

更新

Repository

Presenter

Event
SourcestorageCQRSとイベントソーシングの実装
u

u

u

18

CQRSの実装uRiverpod でUIの動作を受け取るControllerをメソッドとして実装

u

UIイベントを受けてレポジトリへ更新を⾏うが、書き込み結果を待たないで終了

イベントソーシングの実施
uRiverpodでRepositoryの状態変化を伝達するイベントソースを作成

uRiverpodでイベントソースを監視するPresenterを実装。更新が発⽣したらDBを読んで状態変更

画⾯更新の連携u

UIはPresenterを監視して状態変更が発⽣したら画⾯更新

19

ListViewでCQRSを実装してみる
main

providerStartContainer

myApp
MyHomepage
ListItemController

storageStateNotifier
StorageAllUpd

ListItemPresenter

storageNotifier

StorageIndexUpd

CQRSデモ
全体の動作
u

書き込みボタン->レポジトリに書き込み依頼->UI更新せず終了

uレポジトリ書き込み、書き込み終了でイベント発⽣

u

レポジトリのイベント購読->⾃分に関連したイベントなら再読み込み、画⾯更新

結果u

書き込みと読み込みが分離されていて、レポジトリの更新をトリガーに画⾯更新される

20

デモ画⾯
リストアイテムをクリックすると、そのアイテムのみカウンターが増えて更新される

21

ListViewでCQRSを実装してみる
uimport 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.freezed.dart';
part 'main.g.dart';
void main() {final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
final providerStartContainer= ProviderContainer();
final storageStateNotifier=providerStartContainer.read(storageStateProvider.notifier);
storageStateNotifier.clear();runApp(ProviderScope( //プロバイダー監視開始
parent: providerStartContainer,child:const MyApp()));}
class MyApp extends StatelessWidget {const MyApp({super.key});@override
Widget build(BuildContext context) {//APPを定義
return MaterialApp(title: 'RiverPod Demo',theme: ThemeData(useMaterial3: true),home: const MyHomePage(),);}}
class MyHomePage extends StatelessWidget {const MyHomePage({super.key});

22@override
Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Riverpod DEMO'),),
body: Center(child: Consumer(builder: (BuildContext context, WidgetRef ref, Widget? _){final storageAllUpdState=ref.watch(storageAllUpdProvider);final storageStateController=ref.watch(storageStateProvider.notifier);
return ListView.builder(
itemCount: storageStateController.getCount(),itemBuilder: (BuildContext context, int index) {returnConsumer(builder: (BuildContext context, WidgetRef ref, Widget? _) {Final listItemStateController =ref.watch(listItemControllerProvider(index).notifier);
final listItemState = ref.watch(listItemPresenterProvider(index));}debugPrint("$index screen rebuild");
return ListTile(title: Text("Title ${listItemState.title}"),subtitle: Text("Count ${listItemState.subTitle}"),onTap: () {listItemStateController.onTap(index);},);});});}),));}

ListViewでCQRSを実装してみる@freezedclass ListItem with _$ListItem {const factory ListItem({required String title,required String subTitle,}) =_ListItem;}@riverpod
class ListItemController extends _$ListItemController{@override
void build(int index) {}void onTap(int index) {final storageStateNotifier=ref.watch(storageStateProvider.notifier);var dbItem=storageStateNotifier.read(index);
dbItem=dbItem.copyWith(value: dbItem.value+1);
storageStateNotifier.update(index, dbItem);}}@riverpod
class ListItemPresenter extends _$ListItemPresenter{@override
ListItem build(int index) {final storageAllUpdState=ref.watch(storageAllUpdProvider);final storageIndexUpdState=ref.watch(storageIndexUpdProvider(index));
final StorageStateNotifier storageStateNotifier=ref.watch(storageStateProvider.notifier);
final item=storageStateNotifier.read(index);
return ListItem(title: item.title, subTitle: item.value.toString());//freezedなので必ず更新}}@riverpod
class StorageAllUpd extends _$StorageAllUpd {@override
StorageStateValue build(){return StorageStateValue(state: -1);}
void changed() {state=StorageStateValue(state: -1);}}@riverpod
class StorageIndexUpd extends _$StorageIndexUpd {@override
StorageStateValue build(int index){return StorageStateValue(state: index);}
void changed(int index) {state=StorageStateValue(state: index);}}23

ListViewでCQRSを実装してみる
final storageStateProvider = AutoDisposeNotifierProvider<StorageStateNotifier,StorageStateValue>(StorageStateNotifier.new);
final storageProvider = NotifierProvider<StorageNotifier,bool>(StorageNotifier.new);@freezedclass StorageStateValue with _$StorageStateValue {const factory StorageStateValue({required int state,}) =_StorageStateValue;}class StorageStateNotifier extends AutoDisposeNotifier<StorageStateValue>{@overrideStorageStateValue build() {return StorageStateValue(state:-1);}
void clear() {var storageState=ref.watch(storageProvider.notifier).clear();ref.watch(storageAllUpdProvider.notifier).changed();}void create (DbItem dbItem){ref.watch(storageProvider.notifier).create(dbItem);ref.watch(storageAllUpdProvider.notifier).changed();}DbItem read (int index) {return ref.watch(storageProvider.notifier).read(index);}
void update(int index,DbItem dbItem) {ref.watch(storageProvider.notifier).update(index,dbItem);ref.watch(storageIndexUpdProvider(index).notifier).changed(index);}

void delete (int index) {ref.watch(storageProvider.notifier).delete(index);
ref.watch(storageAllUpdProvider.notifier).changed();}int getCount() {return ref.watch(storageProvider.notifier).getCount();}}@freezedclass DbItem with _$DbItem {const factory DbItem({required String title,required int value,}) =_DbItem;}//アイテムリストのストレージ//通常不揮発ストレージに⼊れるが、今回はオンメモリでシミュレート
class StorageNotifier extends Notifier<bool>{
final List<DbItem> _dbItems=[];@overridebool build() {clear();return true;}
void clear() {_dbItems.clear();for (int i=0;i<20;i++){_dbItems.add(DbItem(title:i.toString(),value:0));}}void create (DbItem dbItem){_dbItems.add(dbItem);}
DbItem read (int index) {return _dbItems[index];}
void update(int index,DbItem dbItem) {_dbItems[index]=dbItem;}void delete (int index) {_dbItems.removeAt(index);}int getCount() {return _dbItems.length;}}24

デバッグに便利なテクニック25Observersを定義すると、Riverpodのライフサイクルイベントが表⽰できる
void main() {runApp(ProviderScope(child: MyApp(),observers: [MyObservers]));}
class MyObserver extends ProviderObserver {@override
void didAddProvider(
ProviderBase<Object?> provider,
Object? value,
ProviderContainer container,) {debugPrint('Provider $provider was initialized with $value');}@override
void didDisposeProvider(ProviderBase<Object?> provider,
ProviderContainer container,) {debugPrint('Provider $provider was disposed');}@override
void didUpdateProvider(
ProviderBase<Object?> provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,) {debugPrint('Provider $provider updated from $previousValueto $newValue');}@override void providerDidFail(ProviderBase<Object?> provider,
Object error,
StackTrace stackTrace,
ProviderContainer container,) {debugPrint('Provider $provider threw $error at $stackTrace');}}最後に26

Riverpodを使うといい感じに状態管理ができる
状態管理（＋CQRS＋UI更新）で必要⼗分な画⾯更新を実装可能
デバッグの可視化は重要