Future.microtask & addPostFrameCallback その違い
ときどき見かける呪文
Future.microtask({}());
-
WidgetsBinding.instance.addPostFrameCallback({}());
(またはSchedulerBinding.instance.addPostFrameCallback({}());
)
これらは何?
=== Exception caught by foundation library ===
setState() or markNeedsBuild() called during build.
等、「build
中(≒画面描画中)に状態変更はNG」系のエラーがあります。
その対応として上の2つのメソッドは「画面描画後に処理をコールバックで実行する」ために使用されます。が、
なぜ推奨しないかは以下のremi-sanの警告(スレッド)をご一読ください。
この警告は「addPostFrameCallbackを使うはめになるということは、なにかおかしなことをしているに違いない」ということを示唆しています。
その違い
結論からいうと、
-
Future.microtask({}());
はDartの非同期処理 -
addPostFrameCallback({}());
はFlutterの描画処理
にそれぞれ関係しており、使う目的はだいたい同じでも仕組みは全く異なるものです。
以下ではまずDartの処理について説明し、その後Flutterの描画処理について簡単に記載します。
Dartはシングルスレッドです
Dartは様々な処理を同時並行で実施しているように見えますが、シングルスレッドなので一度に複数の処理は実行できません。
ではなぜそのように見えるかというと、それはEvent Loop
という仕組みを利用して各処理をうまく捌いているからに他なりません。
-
Event Loop
とはスケジュールされた処理を実行する無限ループのことです。 - スケジュールされた処理は2つのキュー(
Event Que
,Microtask Que
)に格納されています(FIFO)。 -
Event Loop
は、これらのキューからひとつずつ処理を取り出すことで、処理を実行していきます。 - 基本的にすべての処理(ボタン操作やネットワーク応答など)は、
Event Que
にevent
として格納されます。 - 特別な設定をすることで、処理を
Microtask Que
にtask
として格納することができます。
その方法のひとつが、処理をFuture.microtask
のコールバックとして設定することです。
2つのキューの実行順は以下のようになります
-
main()
を実行(=main threadを開始)し、Evnet Loop
を回す -
Microtask Que
にtask
があるかチェックし、あれば(Microtask Que
からなくなるまで)task
を実行 -
Evnet Que
のevnet
を1つ実行 - 次の
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
参考にした記事
余談
この記事のきっかけはFlutter.Okinawa#2でLTとして発表したものです。
完全オフラインイベントですが、ご参加お待ちしています。
Discussion