(Flutter 開発を始めたばかりの時にしがちな setState の誤解の話)

flutter create myapp した後、setState に関わるところの記述は以下のようになっています。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    return Text('$_counter');
  }
}

「setState すると build メソッドが呼ばれて画面が更新されます」といったコメントが添えられています。

setState しているところだけ抜き出すと以下です。

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

私は最初これを見たとき、画面更新を起こしたければ setState() の中で _counter++ を実行する必要があるのだと理解しました。

そしてしばらく勉強を続けると以下のような書き方に出会いました。

@override
void initState() {
  ...(省略)...
  controller.addListener(() {
    setState(() {});
  });
}

この中身が空の setState(() {}); がいったい何をするのか理解できませんでした。

先に結論

ということで最初の私の理解は間違っていました。正しくは以下です。

  • setState() に渡すコールバック関数が何であろうが関係なく、setState() を実行すればそのウィジェットの build メソッドが呼び出されて画面が更新される。
  • setState() で囲っているのは、これのために画面変更を起こしたいのですよ、と人間が見てわかりやすくするため。

つまり以下は機能的に全く同じです。

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}
void _incrementCounter() {
  _counter++;
  setState(() {});
}

setState がやっていること

setState のコードを見ればよくわかります。60 行ほどの短い関数です。

簡略化すると setState が行う処理は3つです。

  void setState(VoidCallback fn) {
    assert(.........);
    fn();
    _element!.markNeedsBuild();
  }
  1. アサーション
    • 色々とチェック
  2. 渡されたコールバック関数を実行する
    • ただそのまま実行するだけ
  3. markNeedsBuild() する
    • ウィジェットが Dirty であり再ビルドが必要なことを Flutter のフレームワークに伝える

つまり、コールバック関数はただ実行するだけであり、その関数でどんな処理が行われようが(行われまいが)、markNeedsBuild() し、結果として画面の再描画が実行されます。

ではなぜコールバック関数を渡すようになっているのか

その理由は以下の ISSUE を見るとよくわかります。

Add a “design discussion” section to the setState method · Issue #12296 · flutter/flutter

質問

Why would you want to put all your mutations into a lambda, especially if they are all side-effecting? Wouldn’t it be better just to call update() directly after you do all the state mutations?

回答

We used to just have a markNeedsBuild method but we found people called it like a good luck charm – any time they weren’t sure if they needed to call it, they’d call it.

We changed to a method that takes a (synchronously-invoked) callback, and suddenly people had much less trouble with it. We found people understand much more easily that if you didn’t change any state, you don’t need to call it; if you did change some state, you call it and change the state during the call.

つまりあえて setState にコールバック関数を渡す使い方にした方が、使用者はより明確な意図を持って setState してくれるようになるから、みたいですね。

参考