コンテンツにスキップ

値が変わると、画面が変わる仕組み

値を直接書き換えても、画面は変わらない

Section titled “値を直接書き換えても、画面は変わらない”

FloatSoda では、変数の値を書き換えるだけでは、画面の再構築は予約されず、画面は変わりません。画面を変えるには、SetState() を呼びます。この違いを知らないと、AI が書いたコードで「押しても数字が増えない」ときに、直し方が分かりません。

uGUI では、画面の部品が実体として残っていて、text.text = "..." のように値を直接書き換えます。FloatSoda では、ウィジェットは「こういう画面にしたい」を書いた設計図で、直接は書き換えません。値が変わったら、新しい設計図を作り直して、変わった部分だけを画面に反映します。uGUI から来た人向けの対応は、uGUI から来た人へにあります。

次のコードは、押しても画面が変わらない例です。_count は増えますが、作り直しが予約されないので、Build() は呼ばれません。

OnTap = () => _count++,

SetState() で包むと、画面が変わります。

OnTap = () => SetState(() => _count++),

ウィジェットは作り直される設計図で、状態は State が持つ

Section titled “ウィジェットは作り直される設計図で、状態は State が持つ”

ウィジェットは不変です。一度作ったウィジェットのプロパティは、あとから書き換えられません。変わる値は、ウィジェットではなく State が持ちます。

置き場所は、値が変わるかどうかで決めます。

値の性質 置き場所 例
ウィジェットを作るときに、外から渡す ウィジェットのプロパティ ボタンのラベル、色
画面の中で変わる State のフィールド 押した回数、オンかオフか

渡す値を変えるときは、新しいウィジェットを作ります。

State を持つウィジェットは、StatefulWidget<T> を継承します。State<T> を継承したクラスに、変わる値と Build() を書きます。

public record Counter : StatefulWidget<Counter>
{
public override State<Counter> CreateState() => new CounterState();
}
public class CounterState : State<Counter>
{
private int _count;
public override Widget Build(IBuildContext context) => new GestureDetector
{
OnTap = () => SetState(() => _count++),
Child = new Text($"押した回数: {_count}"),
};
}

Build() は、作り直しのたびに呼ばれ、新しいウィジェットを返します。それでも _count が消えないのは、State が作り直しをまたいで残るためです。

SetState を呼ぶと、次のフレームで何が起きるか

Section titled “SetState を呼ぶと、次のフレームで何が起きるか”

SetState() は、渡された処理を実行して、このウィジェットに再構築を要求します。作り直しは、その場では行われません。予約されたあとの再構築の処理で、変更のあったウィジェットが、親から順に作り直されます。

SetState(...) 状態を変えて、作り直しを予約する
↓ 再構築の処理
Build() が呼ばれる 親から順に、新しいウィジェットを作る
↓
前のウィジェットと比べる
↓
変わった部分だけ、レイアウトと描画内容の記録をやり直す

そのため、SetState() を呼んだ直後の行では、まだ画面は変わっていません。

SetState() を呼ぶ条件は、次のとおりです。

  • FloatSoda のメインスレッドから呼ぶこと

裏で動く処理から画面を変えたいときは、設定やログ、裏で動く処理を足すの方法を使います。

作り直しのとき、古いウィジェットと新しいウィジェットが同じ型で、同じ Key なら、対応する要素が使い回されます。State もそのまま残ります。型か Key が違うと、作り直しではなく入れ替えになり、State は捨てられます。

作り直しても、変わった部分だけが描き直される

Section titled “作り直しても、変わった部分だけが描き直される”

作り直しは、毎フレームの全描画ではありません。SetState() を呼んだウィジェットを作り直し、その結果が変わった部分だけが、レイアウトと描画内容の記録をやり直します。再構築、レイアウト、描画内容の記録は、更新が必要な部分で行います。

このため、値が変わったら SetState() を呼ぶ、という書き方で足ります。ツリーの内部の動きは、コントリビューター向けのフレームワーク層にあります。処理の全体の流れは、FloatSoda の仕組みで読めます。

値の変化が描画だけで済むときは、SetState() を使わずに更新できます。FadeTransition は、AnimationController の値を受け取り、Build() を呼ばずに、描画だけを更新します。ふわっと出す、点滅させる、といった動きに向いています。作り方は、アニメーションを付けるにあります。

祖先の値が変わったときに、その値に依存する子孫を作り直す InheritedWidget もあります。子孫は、DependOnInheritedWidgetOfExactType<T>() で値を参照すると、依存先として登録されます。テーマや設定のように、離れた部品に同じ値を渡す場面で使います。

作り直しが期待どおりに起きないとき

Section titled “作り直しが期待どおりに起きないとき”

画面が変わらない、または意図しないときに状態が初期値へ戻るときは、次の2つの原因を確かめます。

症状 よくある原因 直し方
値は変わったのに、画面が変わらない SetState() を呼んでいない 値を変える処理を SetState(() => ...) で包む
並べ替えたら、値が別の項目に移った 並ぶ子に Key を付けていない 子ごとに Key を付ける。同じ親の子どうしで重複せず、並べ替えの前後で変わらない値を使う。たとえば Key = new ValueKey<string>(name)
names.Select(name => (Widget)new Counter { Key = new ValueKey<string>(name) }).ToList()

反応しない原因の切り分けは、反応しないときに切り分けるで扱います。

画面と状態を、ツール全体でどう分けるかは、画面と状態の分け方に進みます。