1 of 109

なぜわれわれは Riverpod を使うのか

Flutter における状態管理とその課題

ちゅーやん(中條 剛)

スピーカー名

2 of 109

Riverpod 使っていますか?

3 of 109

「あなたのプロジェクト」で採用した理由を

説明できますか?

4 of 109

自己紹介

ちゅーやん(中條 剛)

  • フリーランス の Flutter アプリ開発者
  • アプリ開発研修の 講師 。テキストも作ります
  • Flutter アプリ開発の アドバイザー もやってます
  • 技術記事 も書いてます
  • お仕事のご依頼は X の DMメールアドレス まで

5 of 109

自己紹介

(株)はんぽさき と LivMap

6 of 109

自己紹介

(株)はんぽさき と LivMap

LivMap(リブマップ) とは

チームメンバーが地図上で様々な情報を管理するためのアプリ。

特殊な用途の地図を共有でき、地図上で現在位置・写真・地点情報・軌跡といった情報を共有しながら行動できる。

設備管理業、運輸業、農林水産業、調査・検査、災害対応等で

利用が進んでいます。

7 of 109

自己紹介

(株)はんぽさき と LivMap

LivMap(リブマップ) とは

チームメンバーが地図上で様々な情報を管理するためのアプリ。

特殊な用途の地図を共有でき、地図上で現在位置・写真・地点情報・軌跡といった情報を共有しながら行動できる。

設備管理業、運輸業、農林水産業、調査・検査、災害対応等で

利用が進んでいます。

Riverpod 移行中!

8 of 109

この発表の内容

前半:Flutter における状態管理

  1. Flutter における宣言的な UI 構築とは
  2. Element と markNeedsBuild()
  3. ephemeral state と app state
  4. StatefulWidget と InheritedWidget

9 of 109

この発表の内容

後半:状態管理の課題とRiverpod の戦略

  1. Riverpod とは
  2. 状態の生成、更新、破棄
  3. 状態へのアクセスと更新の検知
  4. 状態同士の連携
  5. 非同期な状態の更新
  6. テスト

10 of 109

前半:Flutter における状態管理

3

11 of 109

Flutter における宣言的な UI 構築とは

UI = f(State)

状態

build()

Widget

Widget

Widget

Widget

Widget

12 of 109

Flutter における宣言的な UI 構築とは

UI = f(State)

状態

build()

Widget

Widget

Widget

Widget

Widget

①「状態」を使ったビルド処理

Flutter フレームワークが StatelessWidget や StatefulWidget の build() メソッド を呼び出す。

その際、build() メソッドで利用するデータのことを「状態」と呼ぶ。

13 of 109

Flutter における宣言的な UI 構築とは

UI = f(State)

状態

build()

Widget

Widget

Widget

Widget

Widget

② Widget ツリーの構築

階層構造を持った複数の Widget が生成される。

生成された Widget は Flutter フレームワークで保持され、 ElementRenderObject といったオブジェクトと一緒に管理される。

14 of 109

Flutter における宣言的な UI 構築とは

UI = f(State)

状態

build()

Widget

Widget

Widget

Widget

Widget

③ レイアウト計算と描画

Widget から生成された RenderObject によって レイアウト計算 が行われ、描画内容が決定し、描画エンジン を通してディスプレイに 描画 される。

15 of 109

Flutter における宣言的な UI 構築とは

UI の更新(ダメな例)

状態

build()

Widget

Widget

Widget

Widget

Widget

16 of 109

Flutter における宣言的な UI 構築とは

UI の更新(ダメな例)

状態

build()

Widget

Widget

Widget

Widget

Widget

UI の更新

ユーザー操作などによって UI を変化させたい 場合、すでに生成されている Widget を直接変更することはできない。(というより Widget を書き換えても意味がない)

17 of 109

Flutter における宣言的な UI 構築とは

UI の更新(正しい方法)

状態

build()

Widget

Widget

Widget

Widget

Widget

新しい状態

Widget

Widget

Widget

Widget

Widget

18 of 109

Flutter における宣言的な UI 構築とは

UI の更新(正しい方法)

状態

build()

Widget

Widget

Widget

Widget

Widget

新しい状態

Widget

Widget

Widget

Widget

Widget

①「状態」を変更してリビルド

状態を変更した上で もう一度 build() メソッドを呼び出す「リビルド」する)。

19 of 109

Flutter における宣言的な UI 構築とは

UI の更新(正しい方法)

状態

build()

Widget

Widget

Widget

Widget

Widget

新しい状態

Widget

Widget

Widget

Widget

Widget

② 生成される Widget が変化する

状態が変わっているため、build() メソッドで生成される Widget の内容が変化 する。

20 of 109

Flutter における宣言的な UI 構築とは

UI の更新(正しい方法)

状態

build()

Widget

Widget

Widget

Widget

Widget

新しい状態

Widget

Widget

Widget

Widget

Widget

③ UI の更新

Widget ツリーの差分を検知して レイアウトの再計算と再描画 が行われ、UI が変化する。

21 of 109

Flutter における宣言的な UI 構築とは

UI の更新(正しい方法)

状態

build()

Widget

Widget

Widget

Widget

Widget

新しい状態

Widget

Widget

Widget

Widget

Widget

ポイント

UI = f(State)

UI は常に 状態(State) を使った

build() メソッド(f) の実行結果 で決定する

22 of 109

Element と markNeedsBuild()

23 of 109

Element と markNeedsBuild()

Element とは

Widget

Element

24 of 109

Element と markNeedsBuild()

Element とは

Widget

Element

① Widget が Element を生成

Flutter フレームワークから createElement() が呼び出され、 Widget が Element を生成 する。

Widget と Element は常に 1 対 1 の関係。

25 of 109

Element とは

Element と markNeedsBuild()

Widget

Element

② Element は Widget の参照を保持する

生成された Element は、Widget への参照を保持 した状態でフレームワーク内で管理される。

26 of 109

Widget ツリーと Element

Element と markNeedsBuild()

Widget

Widget

Widget

Widget

Widget

Element

Element

Element

Element

Element

27 of 109

Widget ツリーと Element

Element と markNeedsBuild()

Widget

Widget

Widget

Widget

Widget

Element

Element

Element

Element

Element

Element ツリー

「Widget ツリー」と呼ばれるが、実際にツリー構造で 親子の参照を保持しているのは Element

BuildContext オブジェクトも実体は Element。

28 of 109

突然の宣伝 FlutterKaigi 2021: Everything is an "Element"

29 of 109

Element とリビルド

Element と markNeedsBuild()

Widget

Widget

Widget

Widget

Widget

Element

Element

Element

Element

Element

Widget

Widget

Widget

Widget

Widget

30 of 109

Element とリビルド

Element と markNeedsBuild()

Widget

Widget

Widget

Widget

Widget

Element

Element

Element

Element

Element

Widget

Widget

Widget

Widget

Widget

Widget の再生成

リビルド時、リビルド範囲内の Widget は基本的に すべて再生成 される。

31 of 109

Element とリビルド

Element と markNeedsBuild()

Widget

Widget

Widget

Widget

Widget

Element

Element

Element

Element

Element

Widget

Widget

Widget

Widget

Widget

Element の再生成

Element はリビルド時に 可能な限り再利用 され、Widget の参照先が新しいものに更新される。

RenderObject やレイアウト計算の結果なども可能な限り再利用される。

32 of 109

Element とリビルド

Element と markNeedsBuild()

Widget

Widget

Widget

Widget

Widget

Element

Element

Element

Element

Element

Widget

Widget

Widget

Widget

Widget

ポイント

Widget のリビルド

RenderObject の再描画

分けて考える

33 of 109

Element と markNeedsBuild()

markNeedsBuild() とリビルド

Widget

Element

markNeedsBuild()

build()

34 of 109

Element と markNeedsBuild()

markNeedsBuild() とリビルド

Widget

Element

markNeedsBuild()

build()

リビルドの発生源

リビルドは Element の markNeedsBuild() メソッドが呼び出される ことで発生する。

35 of 109

Element と markNeedsBuild()

StatelessWidget のリビルド例

Stateless

Widget

Stateless

Element

markNeedsBuild()

build()

36 of 109

Element と markNeedsBuild()

StatelessWidget のリビルド例

Stateless

Widget

Stateless

Element

markNeedsBuild()

build()

StatelessWidget 場合

1. StatelessElement の markNeedsBuild() メソッドが呼び出される

2. StatelessElement が StatelessWidget の build() メソッド を呼び出す。

37 of 109

Element と markNeedsBuild()

StatefulWidget のリビルド例

Stateful

Widget

Stateful

Element

markNeedsBuild()

build()

State

38 of 109

Element と markNeedsBuild()

StatefulWidget のリビルド例

Stateful

Widget

Stateful

Element

markNeedsBuild()

build()

State

StatefulWidget の場合

1. StatefulElement の markNeedsBuild() メソッドが呼び出される

2. StatefulElement が State の build() メソッド を呼び出す。

39 of 109

Element と markNeedsBuild()

Stateful

Widget

Widget

Widget

Widget

Widget

Widget

Staterul

Element

Element

Element

Element

Element

Element

markNeedsBuild()

build()

State

ポイント

どのような状態管理手法であっても

Element markNeedsBuild()

呼び出されることでリビルドが発生する

(リビルドの伝播については割愛します)

40 of 109

ephemeral state と app state

41 of 109

ephemeral state と app state

ephemeral state

Stateful

Widget

Widget

Widget

State

42 of 109

ephemeral state と app state

ephemeral state

Stateful

Widget

Widget

Widget

State

ephemeral state とは

単一の Widget 内でのみ参照される状態のことを ephemeral state と呼ぶ。

local state, UI state とも呼ばれる。

43 of 109

app state

ephemeral state と app state

Widget

Stateless

Widget

Widget

Stateless

Widget

Widget

State

44 of 109

app state

ephemeral state と app state

Stateless

Widget

Widget

Stateless

Widget

State

Widget

Widget

app state とは

複数の Widget の build() メソッドを含むさまざまな箇所から参照可能な状態のことを app state と呼ぶ。

shared state と呼ばれることもある。

45 of 109

StatefulWidget と InheritedWidget

46 of 109

StatefulWidget

StatefulWidget と InheritedWidget

Stateful

Widget

Widget

State

Stateful

Element

1. setState()

2. markNeedsBuild()

3. build()

47 of 109

StatefulWidget

StatefulWidget と InheritedWidget

Stateful

Widget

Widget

State

Stateful

Element

1. setState()

2. markNeedsBuild()

3. build()

StatefulWidget

ephemeral state管理するための標準的な Widget。

setState() を呼び出すと内部的に markNeedsBuild() が呼び出され、リビルドが発生 する。

48 of 109

StatefulWidget と InheritedWidget

InheritedWidget

Inherited

Widget

Inherited

Element

Widget

Element

dependOnInheritedWidgetOfExactType()

State

49 of 109

StatefulWidget と InheritedWidget

InheritedWidget

Inherited

Widget

Inherited

Element

Widget

Element

Element

Element

Element

dependOnInheritedWidgetOfExactType()

State

InheritedWidget

app state を管理するための標準的な Widget。

BuildContext の dependOnInheritedWidgetOfExactType() を呼ぶと、Widget ツリーの祖先に配置された指定した型の InheritedWidget を O(1) で参照できる。

50 of 109

StatefulWidget と InheritedWidget

InheritedWidget

Inherited

Widget

Inherited

Element

Widget

Element

State

Inherited

Widget

State

markNeedsBuild()

build()

51 of 109

StatefulWidget と InheritedWidget

InheritedWidget

Inherited

Widget

Inherited

Element

Widget

Element

Element

Element

State

Inherited

Widget

State

markNeedsBuild()

build()

InheritedWidget のリビルド

InheritedWidget は 参照してきた Element を覚えている ため、自身がリビルドされるとその Element に対して markNeedsBuild() を呼び出す

これによって 状態の変更時に利用側の Widget がリビルドされる 作りになっている。

52 of 109

StatefulWidget と InheritedWidget

InheritedWidget

Inherited

Widget

Widget

Widget

Widget

Widget

Inherited

Element

Widget

Element

Element

Element

Element

Element

State

Inherited

Widget

State

markNeedsBuild()

build()

ポイント

状態には ephemeral stateapp state

という区別がある。

Flutter の標準的な Widget として

StatefulWidgetInheritedWidget

それぞれ用意されている

53 of 109

後半:状態管理の課題とRiverpod の戦略

15

54 of 109

この発表の内容

後半:状態管理の課題とRiverpod の戦略

  • Riverpod とは
  • 状態の生成、更新、破棄
  • 状態同士の連携
  • 非同期な状態の更新
  • テスト

55 of 109

Riverpod とは

  • InheritedWidget の再実装を目指している状態管理パッケージ
    • InheritedWidget を代替するものなので、app state の管理が主な役割
    • ephemeral state を管理する StatefulWidget の代替ではない
  • Widget ツリーは極力利用しない 作りになっている
    • 状態管理の主な処理を担当する riverpod パッケージ と、それを Flutter の Widget から利用するための flutter_riverpod パッケージ に分割されている
    • Widget ツリーのルールとは独立して動いている。�
  • 状態管理における 「よくある課題」への解決策 を幅広く用意してくれている
    • 特に「状態の破棄」と「非同期処理」に対する解決策は Riverpod ならでは。

56 of 109

Riverpod とは

  • InheritedWidget の再実装を目指している状態管理パッケージ
    • InheritedWidget を代替するものなので、app state の管理が主な役割
    • ephemeral state を管理する StatefulWidget の代替ではない
  • Widget ツリーは極力利用しない 作りになっている
    • 状態管理の主な処理を担当する riverpod パッケージ と、それを Flutter の Widget から利用するための flutter_riverpod パッケージ に分割されている
    • Widget ツリーのルールとは独立して動いている。�
  • 状態管理における 「よくある課題」への解決策 を幅広く用意してくれている
    • 特に「状態の破棄」と「非同期処理」に対する解決策は Riverpod ならでは。

今日の本題!

57 of 109

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 )

58 of 109

Riverpod とは

Riverpod とリビルド(概略版)

Provider

Scope

_Provider

ScopeElement

Consumer

Widget

Provider

ScopeState

Consumer

Element

Provider

Container

Provider

Provider

State

flutter_riverpod

riverpod

markNeedsBuild()

build()

State

59 of 109

Riverpod とは

「状態」を構成する 3 つの要素

state にアクセスするためのキー となるもの。

Provider が直接 state を保持しているわけではない。

state を生成、更新する もの。

関数(annotated function)とクラス(Notifier)の 2 つの手段が用意されている。

UI が参照する状態そのもの

immutable(不変)なオブジェクトであることが前提となっている。

Provider

State

メンテナ(※造語)

生成・呼び出し

生成・更新

60 of 109

Riverpod とは

「状態」を構成する 3 つの要素

state にアクセスするためのキー となるもの。

Provider が直接 state を保持しているわけではない。

state を生成、更新する もの。

関数(annotated function)とクラス(Notifier)の 2 つの手段が用意されている。

UI が参照する状態そのもの

immutable(不変)なオブジェクトであることが前提となっている。

Provider

State

メンテナ(※造語)

生成・呼び出し

生成・更新

関数(annotated function)

クラス(Notifier)

61 of 109

状態の生成、更新、破棄

62 of 109

状態の生成

  • 課題
    • 状態の生成は可能な限り利用する直前まで遅延したい
    • まだ使わないデータの API リクエストは避けたい
    • 「最初にそのデータを必要とする画面」はユーザーの操作次第
  • Riverpod の解決方法
    • はじめて Ref を使ってアクセスした時に
  • Provider が生成され(Dart のグローバル変数はデフォルトで late 扱い)
  • 必要に応じて Notifier オブジェクトが生成され
  • 関数 / build() メソッドによって State が生成される
    • アプリのどこかで ref.read / .watch / .listen するまで状態は生成されない

63 of 109

状態の更新

  • 課題
    • 状態の更新を安全に行いたい。
    • 意図しない箇所からの変更、意図しない値への変更を防ぎたい。
  • Riverpod の解決方法
    • 状態の更新処理がメンテナの中で完結する ことを強制される
    • 状態を任意の値に変更可能な手法は非推奨となっている
      • 例)StateProvider は非推奨になった
      • 例)状態は不変なオブジェクトであることが前提の設計になっている

64 of 109

状態の更新

命令的な手法による状態の更新

命令的に状態を更新する際はかならず

  1. ref と Provider を使って Notifier へアクセスし
  2. Notifier に用意されたメソッドを呼び出し

しなければならない。つまり、Notifier で用意した処理でしか状態を更新できない

State

Notifier

State

UI

Provider

ref.read()

更新

65 of 109

状態の更新

命令的な手法による状態の更新

State

Notifier

State

UI

Provider

ref.read()

更新

66 of 109

状態の更新

命令的な手法による状態の更新

State

Notifier

State

UI

Provider

ref.read()

更新

メンテナを通して更新

counterProvider が管理する状態の値は add() メソッドを呼び出すことでしか更新できない

67 of 109

状態の更新

宣言的な手法による状態の更新

State

メンテナ

State

更新

ref.watch で監視

State

状態の変更を検知

68 of 109

状態の更新

宣言的な手法による状態の更新

State

メンテナ

State

更新

ref.watch で監視

State

状態の変更を検知

宣言的な状態の更新

監視している counterProvider の state が更新されたら、それを検知して multipledProvider の state も更新

69 of 109

状態の更新

状態を更新する際は必ず

  • ref と Provider を使ってメンテナ(Notifier オブジェクト)へアクセスし
  • メンテナに用意されたメソッドを呼び出し

しなければならない。つまり、メンテナで用意した処理でしか状態を更新できない

State

メンテナ

State

UI

Provider

ref.read()

更新

ポイント

状態を更新するためには、必ずメンテナ(関数 / Notifier クラス)に実装された処理を通す 必要がある。

これにより、利用する側から任意の値で状態を更新することはできず、意図しない変更を防げる

70 of 109

状態の更新の通知

  • 課題
    • 状態の変更が過不足なく利用箇所へ通知されてほしい
    • ただし、Dart では「任意のオブジェクトのフィールドの値が変化した」ことを検知する手段は無く、やるならクラス定義時に setter の実装が必要。
    • 変更を検知た時、リビルドではなく「命令的」な処理を実行したい場合もある
  • Riverpod の解決方法
    • state = newState の書き方、もしくは リビルド による変更を強制する。
    • 状態オブジェクトの中身を直接書き換えられないよう、状態が immutable である ことを前提としている。
    • 宣言的に監視する ref.watch に加え、命令的な処理を持つ関数のコールバックを待ち受ける ref.listen が用意されている。

71 of 109

状態の破棄

  • 課題
    • 不要になった状態は過不足なく破棄したい。
    • 任意のタイミングでも状態を破棄・再生成したい。
  • Riverpod の解決方法
    • autoDispose の仕組みにより、依存されなくなった状態は自動で破棄される
    • keepAlive オプションにより、破棄されない状態も生成できる
    • ref.invalidate / .refresh により 任意のタイミングで破棄できる
    • 「依存されなくなった」かどうかは リビルドごとに判定 される

72 of 109

状態の破棄

(参考)Widget ツリー上で実現する状態管理の課題

Widget

Inherited

Widget

State

破棄!

Inherited

Widget

State

Widget

73 of 109

状態の破棄

(参考)Widget ツリー上で実現する状態管理の課題

Widget

Inherited

Widget

State

破棄

Inherited

Widget

State

Widget

依存がなくなっても破棄されない

InheritedWidget (その仕組みをベースにした Provider パッケージ)では依存元の Widget が破棄されたとしても「依存されなくなった」ことを検知する仕組みがないため State が自動的に破棄できない。

74 of 109

状態の破棄

Riverpod による autoDispose

Provider

Scope

_Provider

ScopeElement

Consumer

Widget

Provider

ScopeState

Consumer

Element

Provider

Container

Provider

Provider

State

flutter_riverpod

riverpod

State

ref.watch

75 of 109

状態の破棄

Riverpod による autoDispose

Provider

Scope

_Provider

ScopeElement

Consumer

Widget

Provider

ScopeState

Consumer

Element

Provider

Container

Provider

Provider

State

flutter_riverpod

riverpod

State

破棄!

76 of 109

状態の破棄

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

77 of 109

状態同士の連携

78 of 109

状態同士の連携

  • 課題
    • ある状態を元に、別の状態を「宣言的に」生成したい。
    • 元にした状態の変更を検知して、依存している状態も更新したい。
      • 例)「記事一覧データ」を保持する状態と、「絞り込み条件」を保持する状態とから「絞り込まれた記事一覧データ」を保持する状態を生成したい。
    • 複数の状態間での不整合を防ぎたい。
  • Riverpod の解決方法
    • 各メンテナが持つ Ref を利用して、他の状態にアクセスできる。
    • ref.watch を使うことで Widget と同じように 宣言的に状態を更新 できる。

79 of 109

宣言的「ではない」場合

状態同士の連携

① 「すべての記事一覧」を表す状態が変化したら

② 「表示する記事一覧」を表す状態を更新するよう命令 する

AllArticles

Provider

VisibleArticles

Provider

State

State

更新の命令

80 of 109

宣言的「ではない」場合

状態同士の連携

VisibleArticlesProvider が どの Provider に依存していて、どんなタイミングで更新されるか を把握しづらい。

「命令」を忘れると 不整合が発生 する。

AllArticles

Provider

VisibleArticles

Provider

State

State

FilterCondition

Provider

State

更新の命令

更新の命令

81 of 109

宣言的な場合

状態同士の連携

VisibleArticlesProvider が AllArticlesProvider と FilterConditionProvider の状態に依存していることを「宣言」 する。

AllArticles

Provider

VisibleArticles

Provider

State

State

FilterCondition

Provider

State

監視

監視

82 of 109

宣言的な場合

状態同士の連携

83 of 109

宣言的な場合

状態同士の連携

依存する Provider を宣言

依存する Provider が宣言 され、依存先に更新があれば関数が再実行されることが Riverpod により保証されるため、宣言的に利用でき不整合が発生しない。

84 of 109

非同期な状態の更新

85 of 109

非同期な状態の更新

  • 課題
    • 状態の生成に非同期な処理が含まれる場合、状態の生成処理自体に状態が発生する
      • 処理中、処理完了、処理中にエラー発生、リトライ、など。
      • 処理完了を待ってから初めて UI をビルドする、ということはできない
  • Riverpod の解決方法
    • AsyncValue オブジェクトを利用して「状態生成の状態」を管理する
    • 状態の型が Future である場合は、UI が受け取る型は自動的に AsyncValue になる。

86 of 109

非同期な状態の更新

非同期な「状態生成処理の状態」の遷移(一部省略)

処理中

データ取得完了

エラー発生

条件を変えて

取得し直し

同じ条件で

再取得

データ取得完了

エラー発生

1回目

2回目

87 of 109

非同期な状態の更新

非同期な「状態生成処理の状態」の遷移(一部省略)

処理中

データ取得完了

エラー発生

条件を変えて

取得し直し

同じ条件で

再取得

データ取得完了

エラー発生

1回目

2回目

2回目がある!

非同期による状態の生成は 1 度データを取得できたら終わりではない

再取得、条件を変えて取得し直しがあり、さらに成功 / 失敗と続く。

88 of 109

非同期な状態の更新

非同期な「状態生成処理の状態」の遷移(一部省略)

処理中

データ取得完了

エラー発生

条件を変えて

取得し直し

同じ条件で

再取得

データ取得完了

エラー発生

1回目

2回目

AsyncLoading

AsyncError

AsyncData

AsyncData

isLoading: true

isRefreshing: true

AsyncLoading

hasValue: true

isReloading: true

AsyncError

hasValue: true

isLoading: false

AsyncData

isLoading: false

89 of 109

非同期な状態の更新

非同期な「状態生成処理の状態」の遷移(一部省略)

処理中

データ取得完了

エラー発生

条件を変えて

取得し直し

同じ条件で

再取得

データ取得完了

エラー発生

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() メソッドが再度呼び出された場合

90 of 109

非同期な状態の更新

非同期な「状態生成処理の状態」の遷移(一部省略)

処理中

データ取得完了

エラー発生

条件を変えて

取得し直し

同じ条件で

再取得

データ取得完了

エラー発生

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() メソッドが再度呼び出された場合など

91 of 109

非同期な状態の更新

AsyncValue クラスの構造

AsyncLoading

AsyncData

AsyncError

具象クラス

isLoading

データ生成処理中かどうかを表すフラグ

value / hasValue

最後に取得したデータと、取得が一度完了していることを表すフラグ。

error

最後に発生したエラーの内容

保持する値

92 of 109

非同期な状態の更新

AsyncValue クラスの構造

isRefreshing

ロード中だが AsyncLoading ではない場合は true 。

典型的には、 ref.invalidate や ref.refresh した場合に true になる。

isReloading

AsyncLoading だが value や error もある場合は true 。

典型的には、依存する Provider の状態が変化したことによる再処理中に true になる。

AsyncLoading

AsyncData

AsyncError

具象クラス

getter

93 of 109

非同期な状態の更新

非同期な「状態生成処理の状態」の遷移

処理中

データ取得完了

エラー発生

条件を変えて

取得し直し

同じ条件で

再取得

データ取得完了

エラー発生

1回目

2回目

ポイント

非同期に状態を生成する 場合、考慮すべきは「処理中」「データ取得完了」「エラー発生」だけではない。

2 回目以降は上記 3 つの状態のそれぞれに対して

「前回の結果」を考慮した状態管理 が必要になる。

94 of 109

非同期な状態の更新

(おまけ)AsyncValue の内容の遷移図

95 of 109

非同期な状態の更新

ちなみに

Riverpod では 「アプリにおけるほとんどの状態生成が非同期のはず」 という考えの元、

v2 以降は AsyncValue を使った状態管理を前面に押し出している。

96 of 109

テスト

97 of 109

テスト

  • 課題
    • 状態やその生成ロジック単体を unit test でテストしたい。
      • widget test では Widget ごしにテストしなければならず、手間が増える。
  • Riverpod の解決方法
    • riverpod パッケージで分離されている部分で切り分けて unit test できる。
    • 指定した Provider をテストダブルに差し替える仕組みが用意されている。

98 of 109

テスト

99 of 109

テスト

unit test が可能

Provider は test() による単体テストが可能。

テストのために Widget を用意する必要がない。

100 of 109

テスト

101 of 109

テスト

依存関係のある state も unit test 可能

InheritedWidget や Provider パッケージと違い State 同士の連携に Widget ツリーを利用しない ため、他の状態に依存している場合でも変わらず unit test でテストできる。

102 of 109

テスト

103 of 109

テスト

テストダブルへの差し替え

特定の Provider をテストダブルに差し替えることも可能。

状態生成に利用するオブジェクトを他の Provider から受け取る作りにしておけば、 Mock や Fake なオブジェクトに切り替えてテストできる

104 of 109

まとめ

38

105 of 109

まとめ

  • Riverpod を使うことで
    • アプリ開発の状態管理において一般的に発生する課題に対して あらかじめ用意された解決策 を利用できる
    • 課題に対する独自の工夫を考える必要性が薄いため、共通認識を作りやすい
    • アプリ開発の状態管理における 一般的な課題を把握でき、考慮漏れを防げる
  • Riverpod を利用する際の注意
    • 「あらかじめ用意された解決策」自体への理解が必要。
    • 「あらかじめ用意された解決策」が自分のアプリに合っているかの確認が必要
    • 解決策自体がバージョンアップで変化するため、情報のキャッチアップが必要

106 of 109

参考リンク

Riverpod 公式ドキュメント

https://riverpod.dev/

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

https://docs.flutter.dev/resources/inside-flutter

107 of 109

あわせて読みたい / 読んでくれると嬉しい

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

108 of 109

ご清聴ありがとうございました

109 of 109