はじめに
Fltterを使ってTODOアプリの開発を進めることになり、YouTubeで様々な動画を漁っていたのですが、状態管理やUIの構築方法についてわかりやすい動画を見つけたので内容をまとめてみました。今回はUIの更新方法の流れに的を絞って状態管理ついてはまた別記事でまとめようと思います。
参考動画
FlutterKaigi 2023 我々にはなぜ Riverpod が必要なのか - InheritedWidget から始まる app state 管理手法の課題 by ちゅーやん(中條 剛)
※動画後半部分はriverPodという状態管理用のパッケージの説明のため、本記事の内容は前半部分のみ
UI構築イメージ
まずはじめに、FlutterがどのようにUIを構築しているかですが、イメージは以下の図の通りです。
FlutterではWidgetと呼ばれる部品を組み合わせてUIを実装していくわけですが、各Widgetに存在するbuildメソッドが実行されることで、実装内容がUIへ反映されます。
Flutter公式ドキュメント では、"UI = f(State)"と表現されており、
UIは常に状態(State)を持ったbuildメソッド(= f)の実行結果となります。
UIの更新を理解するためには、状態(State)の更新→buildメソッドの呼び出しの流れを理解することが重要になってくるというわけです。
どのようにbuildメソッドが呼び出されているのか?
では、buidメソッドがどのように呼び出されているかというと、"Element"というオブジェクトに定義されているmarkNeedsBuild()
がWidgetのbuildメソッドを呼び出しています。
"Element"とは?
突然出てきた"Element"ですが、UI構築の理解には欠かせないため触れておきます。特徴は以下の通りです。
- Widgetと必ず一対一で生成されるオブジェクト(自身で実装する必要はない)
- Widgetの参照(ツリー)情報を持っている
- =buildメソッドの第一引数(BuildContext型の
context
)
Widgetが生成される際にフレームワークが自動的にcreateElement()
を呼び出し、必ず対の関係となるElementを生成します。生成されたElementはWidgetの親子関係や参照情報を保持しています。
Widgetは頻繁に更新・破棄されるため、参照情報を保持せず、その役割は"Element"に任せているようです。
StatelessWidgetやStatefulWidgetを作成すると、buildメソッドの引数にBuildContext型のcontext
があるのを見たことがあると思います。そのcontext
の実態がElementです。
StatefulWidgetのUI更新の流れ
話を戻して、UI更新したい場合の動きについて見ていきたいと思います。
以下はStatefulWidgetが持つStateを画面に表示しているパターンで、他WidgetからStateを変更した際のUI更新の流れを表しています。
- 他Widgetから変更したいStateを持つStatefulWidgetの
setState()
を呼び出す(ボタン押下時のイベントなど) - 状態が更新されたStateオブジェクトは対で存在するElementの
markNeedsBuild()
を呼び出す - ElementがStatefulWidgetの
build()
を呼び出す
2~3の手順は自身で実装する必要はなく、setState()
を呼び出すことでフレームワークが自動的にUIを更新してくれます。
まとめ
以前業務で使用していたAngularでは、コンポーネント(TypeScript)側で値を変更するとUIが自動更新される双方向バインディングの仕様になっていました。
ただ、FlutterではUIに表示している変数を直接更新してもUIが更新されません。buildメソッドが実行されていないからということですね。
双方向データバインディングでUIを自動更新しないメリットは以下の通りです。
- 不要な再描画をしないことによるパフォーマンスの向上
- UIの更新タイミングを正確に把握できるため、予期せぬ副作用を防げる
- UI更新を必要な時のみ実行できるため、複雑な状態管理やアニメーションの実装において高い柔軟性を持つ
- メモリ使用量を抑えることができる
- 更新のタイミングや問題発生の原因が特定しやすく、デバッグが容易になる