なぜわれわれは Riverpod を使うのか
Flutter における状態管理とその課題
ちゅーやん(中條 剛)
スピーカー名
Riverpod 使っていますか?
「あなたのプロジェクト」で採用した理由を
説明できますか?
自己紹介
ちゅーやん(中條 剛)
自己紹介
(株)はんぽさき と LivMap
自己紹介
(株)はんぽさき と LivMap
LivMap(リブマップ) とは
チームメンバーが地図上で様々な情報を管理するためのアプリ。
特殊な用途の地図を共有でき、地図上で現在位置・写真・地点情報・軌跡といった情報を共有しながら行動できる。
設備管理業、運輸業、農林水産業、調査・検査、災害対応等で
利用が進んでいます。
自己紹介
(株)はんぽさき と LivMap
LivMap(リブマップ) とは
チームメンバーが地図上で様々な情報を管理するためのアプリ。
特殊な用途の地図を共有でき、地図上で現在位置・写真・地点情報・軌跡といった情報を共有しながら行動できる。
設備管理業、運輸業、農林水産業、調査・検査、災害対応等で
利用が進んでいます。
Riverpod 移行中!
この発表の内容
前半:Flutter における状態管理
この発表の内容
後半:状態管理の課題とRiverpod の戦略
前半:Flutter における状態管理
3
Flutter における宣言的な UI 構築とは
UI = f(State)
状態
build()
Widget
Widget
Widget
Widget
Widget
Flutter における宣言的な UI 構築とは
UI = f(State)
状態
build()
Widget
Widget
Widget
Widget
Widget
①「状態」を使ったビルド処理
Flutter フレームワークが StatelessWidget や StatefulWidget の build() メソッド を呼び出す。
その際、build() メソッドで利用するデータのことを「状態」と呼ぶ。
Flutter における宣言的な UI 構築とは
UI = f(State)
状態
build()
Widget
Widget
Widget
Widget
Widget
② Widget ツリーの構築
階層構造を持った複数の Widget が生成される。
生成された Widget は Flutter フレームワークで保持され、 Element や RenderObject といったオブジェクトと一緒に管理される。
Flutter における宣言的な UI 構築とは
UI = f(State)
状態
build()
Widget
Widget
Widget
Widget
Widget
③ レイアウト計算と描画
Widget から生成された RenderObject によって レイアウト計算 が行われ、描画内容が決定し、描画エンジン を通してディスプレイに 描画 される。
Flutter における宣言的な UI 構築とは
UI の更新(ダメな例)
状態
build()
Widget
Widget
Widget
Widget
Widget
Flutter における宣言的な UI 構築とは
UI の更新(ダメな例)
状態
build()
Widget
Widget
Widget
Widget
Widget
UI の更新
ユーザー操作などによって UI を変化させたい 場合、すでに生成されている Widget を直接変更することはできない。(というより Widget を書き換えても意味がない)
Flutter における宣言的な UI 構築とは
UI の更新(正しい方法)
状態
build()
Widget
Widget
Widget
Widget
Widget
新しい状態
Widget
Widget
Widget
Widget
Widget
Flutter における宣言的な UI 構築とは
UI の更新(正しい方法)
状態
build()
Widget
Widget
Widget
Widget
Widget
新しい状態
Widget
Widget
Widget
Widget
Widget
①「状態」を変更してリビルド
状態を変更した上で もう一度 build() メソッドを呼び出す(「リビルド」する)。
Flutter における宣言的な UI 構築とは
UI の更新(正しい方法)
状態
build()
Widget
Widget
Widget
Widget
Widget
新しい状態
Widget
Widget
Widget
Widget
Widget
② 生成される Widget が変化する
状態が変わっているため、build() メソッドで生成される Widget の内容が変化 する。
Flutter における宣言的な UI 構築とは
UI の更新(正しい方法)
状態
build()
Widget
Widget
Widget
Widget
Widget
新しい状態
Widget
Widget
Widget
Widget
Widget
③ UI の更新
Widget ツリーの差分を検知して レイアウトの再計算と再描画 が行われ、UI が変化する。
Flutter における宣言的な UI 構築とは
UI の更新(正しい方法)
状態
build()
Widget
Widget
Widget
Widget
Widget
新しい状態
Widget
Widget
Widget
Widget
Widget
ポイント
UI = f(State)
UI は常に 状態(State) を使った
build() メソッド(f) の実行結果 で決定する
Element と markNeedsBuild()
Element と markNeedsBuild()
Element とは
Widget
Element
Element と markNeedsBuild()
Element とは
Widget
Element
① Widget が Element を生成
Flutter フレームワークから createElement() が呼び出され、 Widget が Element を生成 する。
Widget と Element は常に 1 対 1 の関係。
Element とは
Element と markNeedsBuild()
Widget
Element
② Element は Widget の参照を保持する
生成された Element は、Widget への参照を保持 した状態でフレームワーク内で管理される。
Widget ツリーと Element
Element と markNeedsBuild()
Widget
Widget
Widget
Widget
Widget
Element
Element
Element
Element
Element
Widget ツリーと Element
Element と markNeedsBuild()
Widget
Widget
Widget
Widget
Widget
Element
Element
Element
Element
Element
Element ツリー
「Widget ツリー」と呼ばれるが、実際にツリー構造で 親子の参照を保持しているのは Element。
BuildContext オブジェクトも実体は Element。
突然の宣伝 FlutterKaigi 2021: Everything is an "Element"
Element とリビルド
Element と markNeedsBuild()
Widget
Widget
Widget
Widget
Widget
Element
Element
Element
Element
Element
Widget
Widget
Widget
Widget
Widget
Element とリビルド
Element と markNeedsBuild()
Widget
Widget
Widget
Widget
Widget
Element
Element
Element
Element
Element
Widget
Widget
Widget
Widget
Widget
Widget の再生成
リビルド時、リビルド範囲内の Widget は基本的に すべて再生成 される。
Element とリビルド
Element と markNeedsBuild()
Widget
Widget
Widget
Widget
Widget
Element
Element
Element
Element
Element
Widget
Widget
Widget
Widget
Widget
Element の再生成
Element はリビルド時に 可能な限り再利用 され、Widget の参照先が新しいものに更新される。
RenderObject やレイアウト計算の結果なども可能な限り再利用される。
Element とリビルド
Element と markNeedsBuild()
Widget
Widget
Widget
Widget
Widget
Element
Element
Element
Element
Element
Widget
Widget
Widget
Widget
Widget
ポイント
Widget のリビルド と
RenderObject の再描画 は
分けて考える
Element と markNeedsBuild()
markNeedsBuild() とリビルド
Widget
Element
markNeedsBuild()
build()
Element と markNeedsBuild()
markNeedsBuild() とリビルド
Widget
Element
markNeedsBuild()
build()
リビルドの発生源
リビルドは Element の markNeedsBuild() メソッドが呼び出される ことで発生する。
Element と markNeedsBuild()
StatelessWidget のリビルド例
Stateless
Widget
Stateless
Element
markNeedsBuild()
build()
Element と markNeedsBuild()
StatelessWidget のリビルド例
Stateless
Widget
Stateless
Element
markNeedsBuild()
build()
StatelessWidget の場合
1. StatelessElement の markNeedsBuild() メソッドが呼び出される
2. StatelessElement が StatelessWidget の build() メソッド を呼び出す。
Element と markNeedsBuild()
StatefulWidget のリビルド例
Stateful
Widget
Stateful
Element
markNeedsBuild()
build()
State
Element と markNeedsBuild()
StatefulWidget のリビルド例
Stateful
Widget
Stateful
Element
markNeedsBuild()
build()
State
StatefulWidget の場合
1. StatefulElement の markNeedsBuild() メソッドが呼び出される
2. StatefulElement が State の build() メソッド を呼び出す。
Element と markNeedsBuild()
Stateful
Widget
Widget
Widget
Widget
Widget
Widget
Staterul
Element
Element
Element
Element
Element
Element
markNeedsBuild()
build()
State
ポイント
どのような状態管理手法であっても
Element の markNeedsBuild() が
呼び出されることでリビルドが発生する
(リビルドの伝播については割愛します)
ephemeral state と app state
ephemeral state と app state
ephemeral state
Stateful
Widget
Widget
Widget
State
ephemeral state と app state
ephemeral state
Stateful
Widget
Widget
Widget
State
ephemeral state とは
単一の Widget 内でのみ参照される状態のことを ephemeral state と呼ぶ。
local state, UI state とも呼ばれる。
app state
ephemeral state と app state
Widget
Stateless
Widget
Widget
Stateless
Widget
Widget
State
app state
ephemeral state と app state
Stateless
Widget
Widget
Stateless
Widget
State
Widget
Widget
app state とは
複数の Widget の build() メソッドを含むさまざまな箇所から参照可能な状態のことを app state と呼ぶ。
shared state と呼ばれることもある。
StatefulWidget と InheritedWidget
StatefulWidget
StatefulWidget と InheritedWidget
Stateful
Widget
Widget
State
Stateful
Element
1. setState()
2. markNeedsBuild()
3. build()
StatefulWidget
StatefulWidget と InheritedWidget
Stateful
Widget
Widget
State
Stateful
Element
1. setState()
2. markNeedsBuild()
3. build()
StatefulWidget
ephemeral state を管理するための標準的な Widget。
setState() を呼び出すと内部的に markNeedsBuild() が呼び出され、リビルドが発生 する。
StatefulWidget と InheritedWidget
InheritedWidget
Inherited
Widget
Inherited
Element
Widget
Element
dependOnInheritedWidgetOfExactType()
State
StatefulWidget と InheritedWidget
InheritedWidget
Inherited
Widget
Inherited
Element
Widget
Element
Element
Element
Element
dependOnInheritedWidgetOfExactType()
State
InheritedWidget
app state を管理するための標準的な Widget。
BuildContext の dependOnInheritedWidgetOfExactType() を呼ぶと、Widget ツリーの祖先に配置された指定した型の InheritedWidget を O(1) で参照できる。
StatefulWidget と InheritedWidget
InheritedWidget
Inherited
Widget
Inherited
Element
Widget
Element
State
Inherited
Widget
State
markNeedsBuild()
build()
StatefulWidget と InheritedWidget
InheritedWidget
Inherited
Widget
Inherited
Element
Widget
Element
Element
Element
State
Inherited
Widget
State
markNeedsBuild()
build()
InheritedWidget のリビルド
InheritedWidget は 参照してきた Element を覚えている ため、自身がリビルドされるとその Element に対して markNeedsBuild() を呼び出す。
これによって 状態の変更時に利用側の Widget がリビルドされる 作りになっている。
StatefulWidget と InheritedWidget
InheritedWidget
Inherited
Widget
Widget
Widget
Widget
Widget
Inherited
Element
Widget
Element
Element
Element
Element
Element
State
Inherited
Widget
State
markNeedsBuild()
build()
ポイント
状態には ephemeral state と app state
という区別がある。
Flutter の標準的な Widget として
StatefulWidget と InheritedWidget が
それぞれ用意されている
後半:状態管理の課題とRiverpod の戦略
15
この発表の内容
後半:状態管理の課題とRiverpod の戦略
Riverpod とは
Riverpod とは
今日の本題!
Riverpod とは
Flutter における Riverpod のオブジェクト関係(概略版)
Provider
Scope
_Provider
ScopeElement
Consumer
Widget
Provider
ScopeState
Consumer
Element
Provider
Container
Provider
Provider
State
State
ref.watch()
flutter_riverpod
riverpod
( WidgetRef )
Riverpod とは
Riverpod とリビルド(概略版)
Provider
Scope
_Provider
ScopeElement
Consumer
Widget
Provider
ScopeState
Consumer
Element
Provider
Container
Provider
Provider
State
flutter_riverpod
riverpod
markNeedsBuild()
build()
State
Riverpod とは
「状態」を構成する 3 つの要素
state にアクセスするためのキー となるもの。
Provider が直接 state を保持しているわけではない。
state を生成、更新する もの。
関数(annotated function)とクラス(Notifier)の 2 つの手段が用意されている。
UI が参照する状態そのもの。
immutable(不変)なオブジェクトであることが前提となっている。
Provider
State
メンテナ(※造語)
生成・呼び出し
生成・更新
Riverpod とは
「状態」を構成する 3 つの要素
state にアクセスするためのキー となるもの。
Provider が直接 state を保持しているわけではない。
state を生成、更新する もの。
関数(annotated function)とクラス(Notifier)の 2 つの手段が用意されている。
UI が参照する状態そのもの。
immutable(不変)なオブジェクトであることが前提となっている。
Provider
State
メンテナ(※造語)
生成・呼び出し
生成・更新
関数(annotated function)
クラス(Notifier)
状態の生成、更新、破棄
状態の生成
状態の更新
状態の更新
命令的な手法による状態の更新
命令的に状態を更新する際はかならず
しなければならない。つまり、Notifier で用意した処理でしか状態を更新できない。
State
Notifier
State
UI
Provider
ref.read()
更新
状態の更新
命令的な手法による状態の更新
State
Notifier
State
UI
Provider
ref.read()
更新
状態の更新
命令的な手法による状態の更新
State
Notifier
State
UI
Provider
ref.read()
更新
メンテナを通して更新
counterProvider が管理する状態の値は add() メソッドを呼び出すことでしか更新できない。
状態の更新
宣言的な手法による状態の更新
State
メンテナ
State
更新
ref.watch で監視
State
状態の変更を検知
状態の更新
宣言的な手法による状態の更新
State
メンテナ
State
更新
ref.watch で監視
State
状態の変更を検知
宣言的な状態の更新
監視している counterProvider の state が更新されたら、それを検知して multipledProvider の state も更新
状態の更新
状態を更新する際は必ず
しなければならない。つまり、メンテナで用意した処理でしか状態を更新できない。
State
メンテナ
State
UI
Provider
ref.read()
更新
ポイント
状態を更新するためには、必ずメンテナ(関数 / Notifier クラス)に実装された処理を通す 必要がある。
これにより、利用する側から任意の値で状態を更新することはできず、意図しない変更を防げる。
状態の更新の通知
状態の破棄
状態の破棄
(参考)Widget ツリー上で実現する状態管理の課題
Widget
Inherited
Widget
State
破棄!
Inherited
Widget
State
Widget
状態の破棄
(参考)Widget ツリー上で実現する状態管理の課題
Widget
Inherited
Widget
State
破棄
Inherited
Widget
State
Widget
依存がなくなっても破棄されない
InheritedWidget (その仕組みをベースにした Provider パッケージ)では依存元の Widget が破棄されたとしても「依存されなくなった」ことを検知する仕組みがないため State が自動的に破棄できない。
状態の破棄
Riverpod による autoDispose
Provider
Scope
_Provider
ScopeElement
Consumer
Widget
Provider
ScopeState
Consumer
Element
Provider
Container
Provider
Provider
State
flutter_riverpod
riverpod
State
ref.watch
状態の破棄
Riverpod による autoDispose
Provider
Scope
_Provider
ScopeElement
Consumer
Widget
Provider
ScopeState
Consumer
Element
Provider
Container
Provider
Provider
State
flutter_riverpod
riverpod
State
破棄!
状態の破棄
Riverpod による autoDispose
Provider
Scope
_Provider
ScopeElement
Consumer
Widget
Provider
ScopeState
Consumer
Element
Provider
Container
Provider
Provider
State
flutter_riverpod
riverpod
破棄
依存がなくなったら 破棄される
Riverpod の Provider / Notifier / State は Widget ツリーとは独立しているため、Widget ツリーの上下関係を気にせず破棄できる。
State
状態同士の連携
状態同士の連携
宣言的「ではない」場合
状態同士の連携
① 「すべての記事一覧」を表す状態が変化したら
② 「表示する記事一覧」を表す状態を更新するよう命令 する
AllArticles
Provider
VisibleArticles
Provider
State
State
更新の命令
宣言的「ではない」場合
状態同士の連携
VisibleArticlesProvider が どの Provider に依存していて、どんなタイミングで更新されるか を把握しづらい。
「命令」を忘れると 不整合が発生 する。
AllArticles
Provider
VisibleArticles
Provider
State
State
FilterCondition
Provider
State
更新の命令
更新の命令
宣言的な場合
状態同士の連携
VisibleArticlesProvider が AllArticlesProvider と FilterConditionProvider の状態に依存していることを「宣言」 する。
AllArticles
Provider
VisibleArticles
Provider
State
State
FilterCondition
Provider
State
監視
監視
宣言的な場合
状態同士の連携
宣言的な場合
状態同士の連携
依存する Provider を宣言
依存する Provider が宣言 され、依存先に更新があれば関数が再実行されることが Riverpod により保証されるため、宣言的に利用でき不整合が発生しない。
非同期な状態の更新
非同期な状態の更新
非同期な状態の更新
非同期な「状態生成処理の状態」の遷移(一部省略)
処理中
データ取得完了
エラー発生
条件を変えて
取得し直し
同じ条件で
再取得
データ取得完了
エラー発生
1回目
2回目
非同期な状態の更新
非同期な「状態生成処理の状態」の遷移(一部省略)
処理中
データ取得完了
エラー発生
条件を変えて
取得し直し
同じ条件で
再取得
データ取得完了
エラー発生
1回目
2回目
2回目がある!
非同期による状態の生成は 1 度データを取得できたら終わりではない 。
再取得、条件を変えて取得し直しがあり、さらに成功 / 失敗と続く。
非同期な状態の更新
非同期な「状態生成処理の状態」の遷移(一部省略)
処理中
データ取得完了
エラー発生
条件を変えて
取得し直し
同じ条件で
再取得
データ取得完了
エラー発生
1回目
2回目
AsyncLoading
AsyncError
AsyncData
AsyncData
isLoading: true
isRefreshing: true
AsyncLoading
hasValue: true
isReloading: true
AsyncError
hasValue: true
isLoading: false
AsyncData
isLoading: false
非同期な状態の更新
非同期な「状態生成処理の状態」の遷移(一部省略)
処理中
データ取得完了
エラー発生
条件を変えて
取得し直し
同じ条件で
再取得
データ取得完了
エラー発生
1回目
2回目
AsyncLoading
AsyncError
AsyncData
AsyncData
isLoading: true
isRefreshing: true
AsyncLoading
hasValue: true
isReloading: true
AsyncError
hasValue: true
isLoading: false
AsyncData
isLoading: false
同じ条件で再取得
ref.invalidate() や ref.refresh() によって
build() メソッドが再度呼び出された場合
非同期な状態の更新
非同期な「状態生成処理の状態」の遷移(一部省略)
処理中
データ取得完了
エラー発生
条件を変えて
取得し直し
同じ条件で
再取得
データ取得完了
エラー発生
1回目
2回目
AsyncLoading
AsyncError
AsyncData
AsyncData
isLoading: true
isRefreshing: true
AsyncLoading
hasValue: true
isReloading: true
AsyncError
hasValue: true
isLoading: false
AsyncData
isLoading: false
条件を変えて取得し直し
依存する Provider の状態が変化した ことによって
build() メソッドが再度呼び出された場合など
非同期な状態の更新
AsyncValue クラスの構造
AsyncLoading
AsyncData
AsyncError
具象クラス
isLoading
データ生成処理中かどうかを表すフラグ
value / hasValue
最後に取得したデータと、取得が一度完了していることを表すフラグ。
error
最後に発生したエラーの内容
保持する値
非同期な状態の更新
AsyncValue クラスの構造
isRefreshing
ロード中だが AsyncLoading ではない場合は true 。
典型的には、 ref.invalidate や ref.refresh した場合に true になる。
isReloading
AsyncLoading だが value や error もある場合は true 。
典型的には、依存する Provider の状態が変化したことによる再処理中に true になる。
AsyncLoading
AsyncData
AsyncError
具象クラス
getter
非同期な状態の更新
非同期な「状態生成処理の状態」の遷移
処理中
データ取得完了
エラー発生
条件を変えて
取得し直し
同じ条件で
再取得
データ取得完了
エラー発生
1回目
2回目
ポイント
非同期に状態を生成する 場合、考慮すべきは「処理中」「データ取得完了」「エラー発生」だけではない。
2 回目以降は上記 3 つの状態のそれぞれに対して
「前回の結果」を考慮した状態管理 が必要になる。
非同期な状態の更新
(おまけ)AsyncValue の内容の遷移図
非同期な状態の更新
ちなみに
Riverpod では 「アプリにおけるほとんどの状態生成が非同期のはず」 という考えの元、
v2 以降は AsyncValue を使った状態管理を前面に押し出している。
テスト
テスト
テスト
テスト
unit test が可能
Provider は test() による単体テストが可能。
テストのために Widget を用意する必要がない。
テスト
テスト
依存関係のある state も unit test 可能
InheritedWidget や Provider パッケージと違い State 同士の連携に Widget ツリーを利用しない ため、他の状態に依存している場合でも変わらず unit test でテストできる。
テスト
テスト
テストダブルへの差し替え
特定の Provider をテストダブルに差し替えることも可能。
状態生成に利用するオブジェクトを他の Provider から受け取る作りにしておけば、 Mock や Fake なオブジェクトに切り替えてテストできる。
まとめ
38
まとめ
参考リンク
Riverpod 公式ドキュメント
Riverpod GitHub リポジトリ
https://github.com/rrousselGit/riverpod
Differentiate between ephemeral state and app state | Flutter�https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app
Inside Flutter | Flutter
あわせて読みたい / 読んでくれると嬉しい
Riverpod の AsyncValue をその構造から理解する | Zenn
https://zenn.dev/chooyan/articles/905e5787aeae3b
【Flutter】ref.watch してる state がなぜか破棄されたので調べてみる | Zenn�https://zenn.dev/chooyan/articles/17c13d434d9237
「内側」から理解する Flutter 入門 | Zenn
https://zenn.dev/chooyan/books/934f823764db62
そのリビルド範囲、ホントに狭める必要ある? - Flutter とレンダリングパイプライン | ちゅーやん
https://docs.google.com/presentation/d/1ZbzjTRMR1noCUmgF7siFpAkf9_FZQIoAyU1g49bX4rM/edit#slide=id.p
状態管理を構成する 3 つの要素とそれらが解決したい状態管理の課題 | Zenn
https://zenn.dev/chooyan/articles/b943c6b6b0db6e
ご清聴ありがとうございました