- UIは「buildメソッドの戻り値」の結果によって決定され,
「buildメソッドが状態を参照する」ことで,
その時点でのUI構築結果を計算する.
UIを動的に変化させたい場合は,
状態を変更した上でbuildメソッドを呼び出しなおす(リビルドする)ことで実現する.
- 「UI=f(State)」の考え方に従う.
- UIは常にbuildメソッドの戻り値によってのみ決定する.
- すべてのUIパーツに対する操作は一度「状態を表す値の更新」という処理に一本化され, その上でbuildメソッドにより更新後の値が参照され, 画面全体が最新のものになる.
- どのような経緯があったとしても, 状態とbuildメソッドを確認すれば最終的に構築されるUIが明確になる.
- UIを動的に変更するための経路が, 状態の変更とリビルドに一本化される.
手法 |
公式/3rd |
説明 |
StatefulWidget + setState()とコンストラクタ引数 |
公式 |
ReactのPropsと同じ考え方 |
InheritedWidget |
公式 |
O(1)で子孫ウィジェットから参照する. dependOnInheritedWidgetOfExactType()とgetElementForInheritedWidgetOfExactType() |
InheritedModel |
公式 |
InheritedWidgetのサブクラス. InheritedWidgetによる変更を購読している子孫ウィジェットの中で, 一部のウィジットのみに変更を通知したい場合に使用する. |
StreamBuilder |
公式 |
BLoC (Business Logic of Component)の基礎. デフォルトで非同期処理に対応. StreamBuilderは, StatefulWidgetのサブクラス. |
BLoC + InheritedWidget |
公式 |
StreamBuilderによるBLoCとInheritedWidgetによるStateクラスへの高効率アクセス. |
Redux |
3rd |
一箇所で状態管理. データの更新方向が一方通行. |
Provider |
公式認(3rd) |
InheritedWidgetによる状態管理手法をラッピング |
BLoC + Provider |
公式 |
BLoCとProviderの組み合わせ |
Scoped Model |
公式 |
GoogleのOS(Fuchsia)で利用されているパターン |
Scoped Model + Provider |
公式 |
- |
Riverpot |
3rd |
状態管理手法を完全にFlutterの規則から切り離した独自の手法. React志向. 日本の開発だとこれが多い. |
StatefulWidget + setState() + コンストラクタ引数 による状態管理
- StatefulWidgetを継承したウィジェットクラスは不変なオブジェクト(Stateless)
- 必ず1対1でStateオブジェクトを生成するメソッドを持つ.
- buildメソッドはStateオブジェクトが担当する.
- Stateオブジェクトは, 状態を持つ(可変)であるため任意のフィールドに任意の値を保持, 更新できる.
- 状態更新時のリビルドを発生させるにはStateオブジェクトが持つsetStateメソッドを呼び出す.
- setStateメソッドには, 状態を変化させる処理を持った関数オブジェクトを渡す.
<StatefulWidget> --- <State>・・・状態管理
↑
<ChildWidget>
↑
<AncestorWidget>・・・setState()
-------------------------------------------------
<StatefulWidget> --- <State>・・・状態管理
↑ ↑
this.child widget.child・・・setState()
// Widgetはいつ何時でも状態を持たない
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
@override
State<MyWidget> createState() => _MyState();
}
// Stateオブジェクトが状態を持つ
class _MyState extends State<MyWidget> {
// ユーザーが入力した文字列(状態)
var inputText = '';
@override
Widget build(BuildContext context) {
run MaterialApp(
...
);
}
}
// 以下, ...のどこか
// 状態変化
TextField(
onChanged: (text) {
// 入力内容が変化するたびに呼び出される
inputText = text;
}
),
~~
// 状態更新
setState(() => {
});
<StatefulWidget> ---- <State>・・・状態管理
↑ dependOnInheritedWidgetOfExactType() / getElementForInheritedWidgetOfExactType()
<InheritedWidget>
↑ ↑
<UI_A>. |
|
<UI_*>
// StatefulWidgetクラスにInheritedWidgetクラス経由でStateクラスを参照する関数of()を実装する
e.g.
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key? key,
this.child,
}) : super(key: key);
@override
MyState createState() => MyState();
static MyState of(BuildContext context, {bool rebuild = true}) {
if (rebuild) {
MyInheritedwidget? inheritedWid = context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
return inheritedWid!.data;
}
// リビルドされない
InheritedElement? inheritedElem = const.getElementForInheritedWidgetOfExactType<MyInheritedWidget>();
MyInheritedWidget inheritedWid = inheritedElem!.widget as MyInheritedWidget;
return inheritedWid.data;
メソッドシンボル |
計算時間 |
リビルド | 戻り値 |
利用可能タイミング |
findAncestorWidgetOfExactType |
O(n) |
x |
Widgetのサブクラス |
dependOnInheritedWidgetOfExactType |
O(1) |
⚪︎ |
InheritedWidgetのサブクラス |
getElementforInheritedWidgetOfExactType |
O(1) |
x |
InheritedElementクラス |
findAncestorWidgetOfExactType
- 祖先に当たるWidgetを検索
- 計算時間O(n)
T? findAncestorWidgetOfExactType<T extends Widget>()
dependOnInheritedWidgetOfExactType
- 祖先のInheritedWidgetを検索
- 計算時間O(1)
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>()
getElementForInheritedWidgetOfExactType
- 祖先のInheritedWidgetに対応するElementを取得
- 計算時間O(1)
Element<T extends InheritedWidget>? getElementForInheritedWidgetOfExactType()
- 文字列キー(aspect)を送信元のUIから送って, aspectに該当する子孫ウィジェットだけに更新を伝える.
- フィルタリング関数of()が下記に変わる
// MyStatefulWidget
static MyState of(BuildContext context, String aspect) {
return InheritedModel.inheritFrom<MyInheritedWidget>(context, aspect: aspect)!.data;
StreamBuilderによる状態管理(BLoC)
- BLoCクラスとしてロジックをUIと分離する.
- BLoCパターンは, そもそも非同期対応(Streamクラスを使う)なので,実装の手戻りが少ない.
- BLoCパターンでは, setState()を呼ぶ必要がない.
- BLoCでは, StreamControllerの*.sink.add()を用いて, streamに状態変数を書き込む.
- BLoCでは, stream変数(Stream)を外部公開する.
- BLoCでは, stream変数を更新する非同期関数を定義しておく.
- UI側では, StreamBuilderを使う.
- UI側では, 各Streamクラス毎にStreamBuilderのbuilder関数が呼ばれるだけなので高効率.
- 各WidgetにBLoCクラスをコンストラクタで渡している.
- 解決策としては, InheritedWidget経由でBLoCインスタンスが存在するクラスにアクセスする.
// Process (Interface)
class CountRepository {
Future<int> fetch() {
return Future.delayed(const Duration(seconds: 1)).then((_) {
return 1;
});
}
}
// BLoC
class CounterBloc {
final CountRepository _repository;
final _valueController = StreamController<int>();
final _loadingController = StreamController<bool>();
Stream<int> get value => _valueController.stream;
Stream<bool> get isLoading => _loadingController.stream;
int _counter = 0;
CounterBloc(this._repository) {
_valueController.sink.add(_counter);
_loadingController.sink.add(false);
}
void incrementCounter() async {
_loadingController.sink.add(true);
var increaseCount = await _repository.fetch().whenComplete(() {
_loadingController.sink.add(false);
});
_counter += increaseCount;
_valueController.sink.add(_counter);
}
void dispose() {
_valueController.close();
_loadingController.close();
}
}
~~~
// Blocクラスの初期化
@override
void initState() {
super.initState();
counterBloc = CounterBloc(CountRepository());
}
~~
// StreamBuilderの利用@受信側UI
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
//initialData: 0,
stream: counterBloc.value, // Stream<int>
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return Text(
"${snapshot.data}",
style: Theme.of(context).textTheme.displayMedium,
);
}
);
}
~~
// BLoCの更新メソッドの利用@送信側UI
return ElevatedButton(
onPress:() {
counterBloc.incrementCounter();
},
child: const Icon(Icons.add),
)
BLoC + InheritedWidgetによる状態管理
- BLoC(StreamController + 更新関数 + 状態変数)
- InheritedWidget
- StatefulWidget
- State
<StatefulWidget> ----- <State>
↑ |- <Repo> |- <BLoC>
| |- child |- widget.child
| |- of() |- widget.repo
|
<InheritedWidget>
| ↑
↓ 受信 | 送信
<UI_A> <UI_B>