StatefulWidget
を組み込んだFlutterアプリで画面の表示を動的に変更する場合、State
が持つ変数を更新する(以降、単に変数の更新と言う)必要があり、その更新処理をsetState()
メソッドのコールバック関数の中に書くというのが決まりになっています。ところが、変数の更新処理をコールバック関数の中に書くのではなくsetState()
の外側に書いてみたところ、それでも問題なく画面の表示が更新できました。変数の更新処理をコールバック関数の中に書くというルールが守られていないにも関わらず動いているので、なぜそうなるのか疑問に思い色々調べていました。
カウンターアプリでの具体例
Flutterではおなじみのカウンターアプリを例に具体的な挙動を見ていきます。
画面右下の+ボタン(FloatingActionButton
)のクリックイベントに_incrementCounter()
関数が紐づけられていて、その内部に_counter
(State
の変数)を増やす処理が書かれています。
+ボタンをクリックするとカウンターが1ずつ増えていきます。
通常は変数の更新処理を書いた関数をsetState()
の引数に渡します【①】。
変数の更新処理を書いただけでsetState()
を呼び出さなければ、当然ながらいくら変数が変更されようがそれと連動する画面の表示は何も変わりません【②】。
しかし、先に変数の更新処理を書いてから空の関数を渡したsetState()
を呼んだ場合も画面の表示が更新されています【③】。
【①】setState()の引数に更新処理を渡す
void _incrementCounter() { setState(() { _counter++; }); }
【②】setState()を呼び出さない
void _incrementCounter() { _counter++; }
【③】空の関数を渡したsetState()を呼び出す
void _incrementCounter() { _counter++; setState(() { }); }
コールバック関数内に書くのは「推奨」らしい
コールバック関数内で変数を更新する理由
変数の更新処理をsetState()
のコールバック関数内に書く理由については下記のフォーラムに載っていました。
以前は画面の再構築に必要なmarkNeedsBuild()
メソッドだけがあったが、それが本当に必要かどうか分からない時でもとりあえず呼び出すという使われ方が多かったようです。そこで、State
の更新を画面に反映する方法としてsetState()
を使用するように変更したところ、実装者はsetState()
に変数の更新処理を渡すことだけを考えればよくなり、無駄にmarkNeedsBuild()
が使われることを回避できるようになったという経緯らしいです。
変数の更新処理を絶対にsetState()
のコールバック関数内に書かなければならない、そうしないと動かない、ということではないようです。
変数の更新から再構築までの流れ
setState()
の定義内容を見てみると、fn()
でコールバック関数を実行(実行結果をresult
に代入している)してからmarkNeedsBuild()
を呼び出す作りになっています。
void setState(VoidCallback fn) { // assert文は省略 final Object? result = fn() as dynamic; _element!.markNeedsBuild(); }
次にmarkNeedsBuild()
の定義内容を見てみると、再構築が必要なエレメント(Element
クラス)自身が持つ_dirty
プロパティをtrueにしています。これにより「私(エレメント)は再構築が必要ですよ!」というマークが付けられることになります。その後、自身を引数としてscheduleBuildFor()
を呼んでいます。
void markNeedsBuild() { // assert文は省略 if (_lifecycleState != _ElementLifecycle.active) { return; } if (dirty) { return; } _dirty = true; owner!.scheduleBuildFor(this); }
この先は省略しますが、さらにいくつかのメソッドを経て_dirty
フラグがtrueになっているエレメントがあればbuild()
が呼ばれ再構築されることになります。
結局、再構築を行うために最初に必要になるのはmarkNeedsBuild()
の呼び出しであり、setState()
は変数の更新処理と markNeedsBuild()
の呼び出しをまとめてラッピングしたようなものと考えられます。
色々と余談
setState()を使わない書き方
せっかく用意されているsetState()
を使わない理由は特にないですが、markNeedsBuild()
を直に呼び出す書き方も可能なのでついでに載せておきます。
変数の更新処理の後で、_element
(StatefulElement
オブジェクト)が持つmarkNeedsBuild()
を呼び出せばよいです。
_element
はcontext
として参照できるが、StatefulElement
型にキャストしてmarkNeedsBuild()
が使えるようにしています。
void _incrementCounter() { _counter++; (context as StatefulElement).markNeedsBuild(); }
空のコールバック関数を渡している例
空のコールバック関数を渡したsetState()
を呼び出すのが標準的になっている一例として、AnimationController
やTween
を使ったアニメーションの実行があります。
アニメーションの再生状態の管理などを行うAnimationController
は、State
自身を引数にして(下記のコード例ではvsync
に_MyHomePageState
自身を指定)生成します。
Tween
はアニメーションの開始点と終了点を定義したもので、この範囲内でAnimationController
がState
に含まれるAnimation
を更新します。addListener()
メソッドを使ってフレーム切替えの度に実行したい処理を設定できるので、そこでコールバック関数が空のsetState()
を呼んでいます。
各フレームでのAnimation
の更新はcontroller
が自動的に行なっており、その都度setState()
を呼び出して再構築しなければアニメーションは動きません。しかし、フレーム毎にAnimation
の更新処理をsetState()
のコールバック関数に収めるといったような書き方ができないため、状態の更新とsetState()
の呼び出しが別々になっています。
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { late Animation<double> animation; late AnimationController controller; @override void initState() { super.initState(); controller = AnimationController( vsync: this, duration: const Duration(seconds: 5), ); animation = Tween<double>(begin: 0, end: 10.0) .animate(controller) ..addListener(() { // setStateを呼ばないとアニメーションが動かない setState(() { }); }); controller.repeat(reverse: false); } // 省略 }