Kodokon kodokon.com

Dartの非同期:Future、async/await、FutureBuilder

UIをブロックしたり、うっかりリクエストを再発火させたりすることなく、Future、async/await、FutureBuilderを使いこなしましょう。

10 分 · 3 問

このレッスンを Kodokon で開く

Dartはあなたのアプリケーションを、イベントループに駆動される単一のスレッドの上で実行します。Futureはスレッドではありません。これからやってくる値の約束です。awaitはそのスレッドをブロックすることは決してありません。現在の関数を中断し、制御をループへ返し(UIは動き続けます)、結果が届いたときに再開します。当然の帰結として、async/awaitは純粋な計算については何も並列化しません。CPUを集中的に使う作業には、アイソレート(isolate)が必要です。

DART
Future<double> fetchRate() async {
  await Future<void>.delayed(
    const Duration(milliseconds: 300),
  );
  return 1.08;
}

Future<void> main() async {
  try {
    final rate = await fetchRate();
    print('EUR/USD rate: $rate');
  } on Exception catch (error) {
    print('Rate loading failed: $error');
  }
}
非同期関数はawaitとtry/catchで消費される。

連続する二つのawait直列に実行されます。二つ目は一つ目のあとにしか始まりません。処理が互いに独立しているときは、Future.waitでまとめて起動しましょう。知っておくべきトレードオフは、Future.waitがフェイルファストであることです。最初のエラーが全体を拒否し、ほかのfutureの結果は、たとえ成功していても失われます。部分的な結果が必要なら、それぞれのfutureのなかでエラーを処理しましょう。

DART
Future<String> fetchProfile() async => 'profile';

Future<String> fetchOrders() async => 'orders';

Future<void> main() async {
  final [profile, orders] = await Future.wait(
    [fetchProfile(), fetchOrders()],
  );
  print('$profile / $orders');
}
Future.waitとDart 3の分割代入による並列化。

UIの側では、FutureBuildersnapshotを介してFuturebuildにつなぎます。現場での一番の落とし穴は、futureをbuildのなかで直接作ってしまうことです。リビルドのたびに、親からの単なるsetState一つで十分ですが、リクエストが再発火してしまいます。futureはinitStateに保存し、常にデータより先にエラーをテストしましょう。

DART
import 'package:flutter/material.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

  @override
  State<ProfileScreen> createState() =>
      _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  late final Future<String> _nameFuture;

  @override
  void initState() {
    super.initState();
    _nameFuture = _loadName();
  }

  Future<String> _loadName() async {
    await Future<void>.delayed(
      const Duration(seconds: 1),
    );
    return 'Ada Lovelace';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _nameFuture,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (!snapshot.hasData) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
        return Text(snapshot.data!);
      },
    );
  }
}
futureは一度だけ、initStateのなかで作られる。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. awaitはメインスレッド上で実際には何をしますか?
    • Futureが解決するまでスレッドをブロックする。
    • 計算を自動的に別のスレッドへ移す。
    • 現在の関数を中断し、制御をイベントループへ返す。UIは動き続ける。
  2. Future.wait([a(), b()])で、a()が失敗するとどうなりますか?
    • Future.waita()のエラーで拒否され、b()の結果は失われる。
    • Future.waitb()が終わるのを待ち、その結果だけを返す。
    • b()が成功する限り、エラーは無視される。
  3. FutureBuilderfuture: fetchUser()buildのなかに直接書くのを避けるのはなぜですか?
    • buildは非同期呼び出しを含められず、コードがコンパイルされないから。
    • リビルドのたびに新しいFutureが作られ、そのためネットワークリクエストが再発火するから。
    • snapshotが待機状態に固まったままになるから。