【Flutter】Microtask徹底解説で非同期処理を制御

対象者

  • Flutter/Dart 実務経験者
  • Microtaskを知りたい人
  • 非同期処理やイベントループの詳しい挙動を学びたいエンジニア

はじめに

Flutterアプリ開発では、Futureを使った非同期処理が一般的ですが、同じdart:asyncに属する「Microtask」は扱い方が少し異なります。実務では「軽量な後処理を即座に行いたい」といった場面でMicrotaskを活用します。本記事では、Microtaskと通常のFutureの違いを深掘りし、実務で役立つユースケース、動作確認のテストコードまでお届けします。

Microtaskとは何か

Dartのイベントループには「Microtaskキュー」と「Eventキュー」があり、MicrotaskはEventよりも高い優先度で処理されます。scheduleMicrotaskFuture.microtaskで登録された処理は、現在のイベントループ中に他のMicrotaskが残っていないかぎりすぐに実行されます。

Futureとの違い

イベントループ内での実行順序

  • MicrotaskキューscheduleMicrotaskFuture.microtaskで登録
  • EventキューFuture(() => …)Timer.run相当)で登録
  1. 「同期処理」がすべて終わる
  2. Microtaskキュー内のタスクをFIFOで実行
  3. Eventキュー内のタスクを一つだけ実行し、次のイベントループへ

スケジューリングの仕組み

  • Future.microtask(computation) → 内部で scheduleMicrotask を呼び出し、Microtaskキューへ
  • Future(computation) → 内部で Timer.run を呼び出し、Eventキューへ
  • scheduleMicrotask(callback) → 直接Microtaskキューへ追加

Microtaskのパフォーマンス特性

  • 即時性:現在のイベントループ内で最優先実行
  • 軽量処理向き:短い計算やState更新などに適合
  • 注意点:Microtaskキューが空にならないとEventキューの処理(UI描画など)が遅延し、アプリの応答性低下を招くリスクあり

実務での典型的ユースケース

initState内での初期化遅延

@override
void initState() {
  super.initState();
  Future.microtask(() {
    context.read<MyNotifier>().fetchData(); 
  });
}

initState内で直接context.read<MyNotifier>().fetchData()を呼び出すと、まだウィジェットツリーのビルド前に状態更新が発生し、contextが不安定なタイミングで参照されるためエラーや不整合を起こす可能性があります。
そこでFuture.microtaskを使うことで、同一イベントループ内のマイクロタスクサイクルに処理を遅延させ、ビルド完了後(=安定したcontextが利用可能なタイミング)に安全にデータフェッチを実行できます。

このパターンにより、

  • ビルド中のcontext参照エラーを回避
  • 同一フレーム内で即時かつ軽量に非同期処理を行う
  • UI描画前後のタイミングを厳密に制御

といったメリットを得られます。

テストにおけるタイミング制御

以下のテストコード例を使い、MicrotaskとFutureの順序を厳密に検証できます。

Flutter Testで挙動を確認するコード例

以下のテストコード例を使い、MicrotaskとFutureの順序を厳密に検証できます。

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('micro taskの順番確認', () async {
    final result = [];
    result.add('start');
    scheduleMicrotask(() => result.add('micro task1'));
    final future = Future(() => result.add('future'));
    scheduleMicrotask(() => result.add('micro task2'));
    result.add('end');

    // microtask キューを空にする
    await Future.microtask(() {});
    expect(result.length, 4);
    expect(result, ['start', 'end', 'micro task1', 'micro task2']);

    // 次のイベントループを待って Future を実行
    await Future<void>.delayed(Duration.zero);
    expect(result.length, 5);
    expect(result, ['start', 'end', 'micro task1', 'micro task2', 'future']);
  });
}

注意点とベストプラクティス

  • Microtaskの使いすぎ注意:無限に登録するとUI更新を阻害
  • 粒度の調整:重い処理はcomputeIsolate、あるいはFuture.delayedに分散
  • 明示的なキュー制御:テストでは await Future.microtask(...)await Future.delayed(Duration.zero) を併用

参考

Q&A

Q1. MicrotaskとFutureの違いは何ですか?

A: Microtaskは現在のイベントループ内で即実行され、Future(Timer.run相当)は次のイベントループで実行されます。

Q2. Microtaskを多用すると何が問題になりますか?

A: Microtaskキューが空にならないと他のイベント(UI描画やタイマーなど)が実行されず、アプリが固まるリスクがあります。

Q3. 実務でMicrotaskをどんな場面で使うべきですか?

A: 次のフレーム描画前に完了させたい軽量処理に実施します

まとめ

本記事では、Dart/FlutterにおけるMicrotaskの仕組みとFutureとの違いを解説しました。実務では「今すぐ軽量に非同期処理したい」場合にMicrotaskが力を発揮しますが、使いすぎには注意が必要です。適切な粒度でMicrotaskを活用し、UIの応答性とテストの正確性を両立させましょう。