Future.microtask & addPostFrameCallback その違い

2023/05/16に公開

ときどき見かける呪文

  1. Future.microtask({}());
  2. WidgetsBinding.instance.addPostFrameCallback({}());
    (またはSchedulerBinding.instance.addPostFrameCallback({}());)

これらは何?

=== Exception caught by foundation library ===
setState() or markNeedsBuild() called during build.

等、「build中(≒画面描画中)に状態変更はNG」系のエラーがあります。
その対応として上の2つのメソッドは「画面描画後に処理をコールバックで実行する」ために使用されます。が、

なぜ推奨しないかは以下のremi-sanの警告(スレッド)をご一読ください。
この警告は「addPostFrameCallbackを使うはめになるということは、なにかおかしなことをしているに違いない」ということを示唆しています。

その違い

結論からいうと、

  1. Future.microtask({}());Dartの非同期処理
  2. addPostFrameCallback({}()); Flutterの描画処理

にそれぞれ関係しており、使う目的はだいたい同じでも仕組みは全く異なるものです。

以下ではまずDartの処理について説明し、その後Flutterの描画処理について簡単に記載します。

Dartはシングルスレッドです

Dartは様々な処理を同時並行で実施しているように見えますが、シングルスレッドなので一度に複数の処理は実行できません。

ではなぜそのように見えるかというと、それはEvent Loopという仕組みを利用して各処理をうまく捌いているからに他なりません。

  • Event Loopとはスケジュールされた処理を実行する無限ループのことです。
  • スケジュールされた処理は2つのキュー(Event Que,Microtask Que)に格納されています(FIFO)。
  • Event Loopは、これらのキューからひとつずつ処理を取り出すことで、処理を実行していきます。
  • 基本的にすべての処理(ボタン操作やネットワーク応答など)は、Event Queeventとして格納されます。
  • 特別な設定をすることで、処理をMicrotask Quetaskとして格納することができます。
    その方法のひとつが、処理をFuture.microtaskのコールバックとして設定することです。

2つのキューの実行順は以下のようになります

  1. main()を実行(=main threadを開始)し、Evnet Loopを回す
  2. Microtask Quetaskがあるかチェックし、あれば(Microtask Queからなくなるまで)taskを実行
  3. Evnet Queevnetを1つ実行
  4. 次のEvnet Loopが回る

以上を図で表すと下のようになります。

出典:https://oleksandrkirichenko.com/blog/delayed-code-execution-in-flutter/

Binding

Bindingには様々な種類がありますが、FlutterがWidgetを描画する際にEngineとFrameworkのWidget層を糊付けすることで互いに通信できる状態にする(=WidgetsBinding)など、Flutterの画面描画プロセスで不可欠なセットアップ処理のことをいいます。

ちなみに、Dartの開始点はmain()ですが、Flutterの開始点はrunApp()であり、runApp()内ではこの各種セットアップ処理を初期化しています。

  • つまり画面が描画されるというのは各種Bindingが完了したあとのことです。
  • その「完了」を検知するのが addPostFrameCallbackです。
  • そのため、addPostFrameCallbackに登録されたコールバックが実行される際にはcontextの存在や widgetのbuildの完了が保証されています。また登録された処理はFrame単位で(Frame終了時に)1度だけ実行されます。

BindingやEngine&Frameworkなどの描画処理の詳細については、有益な記事が数多く存在するのでそちらを参照してください。

よくある実験

コードはDelayed code execution in Flutterから頂戴したものです。
以下のようにinitStateに追加した各種処理の順序を、loggerパッケージを使用して確認します。

  
  void initState() {
    super.initState();

    Timer.run(() {
      logger.d("Timer");
    });

    WidgetsBinding.instance.addPostFrameCallback((_) {
      logger.d("WidgetsBinding");
    });

    Future<void>.microtask(() {
      logger.d("Future Microtask");
    });

    SchedulerBinding.instance.addPostFrameCallback((_) {
      logger.d("SchedulerBinding");
    });

    scheduleMicrotask(() {
      logger.d("scheduleMicrotask");
    });

    Future<void>(() {
      logger.d("Future");

      Future<void>.microtask(() {
        logger.d("Microtask from Event");
      });
    });

    Future<void>.delayed(Duration.zero, () {
      logger.d("Future.delayed");

      Future<void>.microtask(() {
        logger.d("Microtask from Future.delayed");
      });
    });
  }

結果

I/flutter (29726): │ 🐛 Future Microtask //Microtask Que:scheduleMicrotaskのラッパー
I/flutter (29726): │ 🐛 scheduleMicrotask //Microtask Que
I/flutter (29726): │ 🐛 WidgetsBinding //Event Que:addPostFrameCallback
I/flutter (29726): │ 🐛 SchedulerBinding // Event Que:addPostFrameCallback
I/flutter (29726): │ 🐛 Timer //Event Que
I/flutter (29726): │ 🐛 (1)Future //Event Que :Timerのラッパー 
I/flutter (29726): │ 🐛 (2)Microtask from Event //Microtask Que:(1)で登録したtask
I/flutter (29726): │ 🐛 (3)Future.delayed  //Event Que: Futureの遅延 eventなので(2)の後に実行
I/flutter (29726): │ 🐛 (4)Microtask from Future.delayed  //Microtask Que:(3)で登録したtask

参考にした記事

https://oleksandrkirichenko.com/blog/delayed-code-execution-in-flutter/
https://medium.com/@purvajg/microtasks-and-event-loops-in-dart-9f5863f031d8
https://betterprogramming.pub/demystifying-flutter-bindings-6a4ac9b64761

余談

この記事のきっかけはFlutter.Okinawa#2でLTとして発表したものです。
完全オフラインイベントですが、ご参加お待ちしています。
https://flutter-okinawa.connpass.com/

Discussion