【Flutter】 setState() とは何か
この記事は Flutter #3 アドベントカレンダー 2020 - Qiita 8 日目の記事です。
Flutter でアプリ開発をする上で StatefulWidget
の setState()
を使ったことが無い、という方はおそらくいないでしょう。StatefulWidget の「状態」を管理する State
に対して、その状態が変化したことを教えて画面の再描画を依頼するアレです。
Flutter のチュートリアルでも真っ先に出てくる、誰もが知っているこの State.setState
ですが、この記事では Flutter のソースコードを読みながら具体的に Element ツリー上で何が起きて画面が更新されているのかを見ていきたいと思います。
これが何の役に立つのかと言われると微妙なところですが、例えば「setState()
って細かく何度も呼んじゃって良いの?」のような疑問がもしかしたら解決するかもしれませんので、ちょっとした読み物として軽く読んでみていただければと思います。もしかしたら Flutter の「3つのツリー」をさらに理解する手助けにもなるかもしれません。
参考
この記事は、上の記事の内容を理解していることを前提に書いています。まだ読んでいなくてもある程度は理解できるように書こうと思いますが、先に上の記事を読んでいただけると、よりスムーズに内容が頭に入ってくるかと思います。
本文
State.setState の流れを確認する
まずは、 State.setState
の流れをざっと追ってみましょう。まずは State.setState
の実装です。[1]
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element!.markNeedsBuild();
}
コメントや assert
を除くとこれだけです。引数として受け取った状態を変化させる関数 fn
を実行して[2]、 _element
の markNeedsBuild()
を呼び出しています。
1行目は引数に渡した関数を実行するだけですので、「画面を更新する」にあたる処理はさらに markNeedsBuild
の中にあります。 Element.markNeedsBuild
の実装は以下の通りです。
void markNeedsBuild() {
if (_lifecycleState != _ElementLifecycle.active)
return;
if (dirty)
return;
_dirty = true;
owner!.scheduleBuildFor(this);
}
最初の if 文で、ライフサイクルの確認( .active
でなければ何もしない)と、すでに dirty
かどうかの確認(すでに dirty
であれば何もしない)をした上で、メインの処理として _dirty
フラグを true
にして owner.scheduleBuildFor()
を呼び出す処理が書かれています。_dirty
フラグや owner
については後述します。
その後、 BuildOwner.scheduleBuildFor
では何をやっているかというと、
void scheduleBuildFor(Element element) {
if (element._inDirtyList) {
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
ということで、ステップ自体はいくつかありますが、メインは最後2行目の _dirtyElements
に element
を追加する処理と、 element
の _inDirtyList
フラグを true
にする処理です。
さて、これ以上呼び出している関数はないのでここで setState()
によって実行される処理は以上です。見た感じ Element や BuildOwner などの各オブジェクトのフィールドの内容を変更しているだけのようですが、結局この一連の処理がどのように画面の更新につながるのでしょうか。それをここから説明していきます。
BuildOwner と dirty について
BuildOwner は、 Widget ツリーや Element ツリーの根元を保持する WidgetsBinding のシングルトンインスタンスによって保持されているオブジェクトで、Widget ツリー(とドキュメントに書いてあるけど実際は Element ツリー)全体のビルドを管理しているオブジェクトです。
Inside Flutter によると、 Flutter はフレームごとに全 Element をリビルドしているわけではなく、主にユーザーの操作などによって描画内容の再計算が必要になった Element を "dirty な" Element としてマークしておき、フレームごとの更新処理ではその dirty な Element だけをリビルド対象にすることで処理を効率化しています。
その dirty な Element のリストを管理しているのが上記の BuildOwner で、ソースコードを見てみるとそのリストは _dirtyElements
というフィールドで保持されています。
それを踏まえて先ほどの BuildOwner.scheduleBuildFor
を見てみると、
_dirtyElements.add(element);
element._inDirtyList = true;
ということで、確かに element
(この element
は setState
が呼び出された StatefulWidget とペアになっている Element) を _dirtyElements
に追加していますね。
ちなみに、 element
の _inDirtyList
フラグにも true
を代入することで、 element
自身も自分が _dirtyList
に入っているかどうかを判断できるようになっています。[3]
dirty な Element の更新処理
BuildOwer の _dirtyElements
リストに入れられた Element は、フレームごとに WidgetBindings によって呼ばれる BuildOwner.buildScope
の中で処理されます。
void buildScope(Element context, [ VoidCallback? callback ]) {
...省略
while (index < dirtyCount) {
try {
_dirtyElements[index].rebuild();
} catch (e, stack) {
...省略
}
index += 1;
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
dirtyCount = _dirtyElements.length;
while (index > 0 && _dirtyElements[index - 1].dirty) {
index -= 1;
}
}
}
}
..省略
}
このメソッドは assrt や例外処理、前処理後処理等を省略してもこの通り長いのですが、メインは while 文で _dirtyElements_
の中身ひとつひとつに対して rebuild()
を呼び出している部分です。
Element.rebuild
は、 Widget の child
, children
や build()
の戻り値を元に Element ツリーを作り直すメソッドです。各 Element が参照している Widget の変化具合によって 既存の Element や RenderObject が使いまわされたり入れ替えられたり[4]しつつ、Widget も先述の fn()
が呼び出されたあとの最新の状態を使って再生成され、それを元に RenderObject を経由して画面の描画が行われていくため、この rebuild
が呼ばれることで画面の表示内容が変化する、というわけです。
まとめ
以上が、普段のアプリ開発で何気なく使う State.setState
によって画面が更新される仕組みです。
setState()
が直接その先で描画の更新処理をしているわけではなく、一旦 State が保持するデータを変更し、紐づいている Element の _dirty
フラグを立て、あとは Flutter フレームワークによって毎フレーム定期的に呼ばれている BuildOwner.buildScope
がうまく 「dirty な」 Element を整理し、まとめて効率的に Widget の再生成とその内容に沿った Element ツリーの更新を行う、ということでした。
その結果として、 最新のツリーの内容が RenderObject を通じて画面の表示処理につながるわけですね。ちなみに "Inside Flutter" によると、この Widget のビルド -> RenderObject によるレイアウト計算 -> 表示の流れを "build-then-layout-then-paint pipeline" と呼ぶそうです。
これを知ったから setState()
の使い方が変わるというわけでもありませんが、例えば setState を何度も何度も呼び出したらその度にツリーの更新や描画のし直しが走るわけでないことが見えてくることで、無理してひとつの setState
に全ての状態更新処理を詰め込まなくても良いことが見えてきたりするのではないでしょうか。
-
この記事で説明する処理の流れに直接関係のない assert 処理は割愛しています。 ↩︎
-
fn
の戻り値result
は、省略した assert 処理で利用します。result
がFuture
の場合(つまりsetState()
に渡す関数が async の場合)はFlutterError
が発生し、setState()
の中で async な処理をするのではなく、 先に async な処理を呼び出し、その結果の中でsetState()
を呼ぶようメッセージが出力されます。 ↩︎ -
このような管理する側、される側の双方で同じ情報を持っておく方法は、 Flutter フレームワークのソースコードを読んでいるとちょくちょく見かけます。 ↩︎
-
このあたりはまた説明が必要になるので別の記事に書きたいと思います。 ↩︎
Discussion