Dart 非同期プログラミング

非同期プログラミングは、現代のアプリ開発において最も中核となる能力の一つです。

ネットワークリクエスト、ファイルの読み書き、データベース操作のいずれも、メインスレッドのブロッキングを避けるために非同期処理が必要です。

この章では、Future の概念、async/await 構文、そして非同期エラーの処理方法を紹介します。


なぜ非同期プログラミングが必要か

まず同期と非同期の違いを理解しましょう。

同期コードは1行ずつ順番に実行され、前の行が完了するまで次の行は始まりません。

ある操作が待機を必要とする場合(ネットワークリクエストなど)、プログラム全体が停止してしまいます。

実例

同期コードと非同期コードの実行結果を比較します:

// 時間のかかる操作をシミュレート
String fetchDataSync() {
  // 同期コード:ここでは2秒待つと仮定
  // この2秒間、プログラム全体は何もできない
  sleep(Duration(seconds: 2));  // ブロッキング待機
  return 'データ取得完了';
}

// 非同期バージョン:メインスレッドをブロックしない
Future<String> fetchDataAsync() async {
  // 非同期待機:この2秒間、プログラムは他のタスクを実行できる
  await Future.delayed(Duration(seconds: 2));
  return 'データ取得完了';
}

void main() async {
  print('同期データ取得開始...');
  var syncResult = fetchDataSync();
  print('同期結果: $syncResult');
  print('(注意:同期中はプログラムが停止している)');

  print('\n非同期データ取得開始...');
  print('リクエスト送信後、プログラムは他のタスクを実行し続ける...');
  var asyncResult = await fetchDataAsync();
  print('非同期結果: $asyncResult');
  print('(非同期中にプログラムは停止していない)');
}
同期データ取得を開始...
同期結果: データ取得完了
(注意:同期中はプログラムがフリーズしました)

非同期データ取得を開始...
リクエストを送信した後、プログラムは他のタスクを継続して実行します...
非同期結果: データ取得完了
(非同期中はプログラムはフリーズしませんでした)

Dart では、すべての I/O 操作(ファイル、ネットワーク、データベース)は非同期方式を使用する必要があります。同期 I/O は Isolate 全体をブロックし、Flutter アプリでは UI がカクつきます。


Future の概念

Future「未来のある時点で完了する」値を表します。

これは約束のようなものです:今は結果がなくても、将来必ず T 型の値(またはエラー)を渡します。

Future には3つの状態があります:

状態説明
未完了(Uncompleted)非同期操作は進行中
完了して値を持つ(Completed with data)操作が成功し、Future が結果値を保持している
完了したがエラー(Completed with error)操作が失敗し、Future がエラー情報を保持している

実例

Future の作成と使用:

void main() {
  print('プログラム開始');

  // Future を作成する
  Future<String> future = Future(() {
    // この関数は将来のある時点で実行される
    return 'サンプル 非同期結果';
  });

  print(Future は作成されたが、まだ完了していない);

  // コールバックを登録:Future が完了したときに実行
  future.then((result) {
    print('結果を受信: $result');
  });

  print('プログラムは実行を継続...');
}
程序开始
Future 已创建,但还没完成
程序继续执行...
收到结果: サンプル 异步结果

出力の順序に注目:最初に「プログラムは実行を継続」が出力され、次に「結果を受信」が出力されます。

これは、Future のコールバックが非同期に実行され、後続のコードをブロックしないことを示しています。


async / await 構文

async と await は、Dart で非同期操作を扱うための中心的な構文です。

async は関数を非同期関数としてマークし、await は Future の完了を待ってその結果を取得します。

実例

// async キーワードは関数を非同期関数としてマークする
// 非同期関数の戻り値の型は Future<T> でなければならない
Future<String> fetchUserName(int id) async {
  // await は Future の完了を待って、その値を取得する
  // 待機中、現在の関数は一時停止するが、他のコードはブロックしない
  await Future.delayed(Duration(seconds: 1));
  return 'ユーザー$id';
}

Future<int> fetchUserAge(int id) async {
  await Future.delayed(Duration(milliseconds: 500));
  return 20 + id;
}

// 複数の非同期操作を順番に実行
Future<void> printUserInfo(int id) async {
  print('ユーザー情報の取得を開始...');

  // 1つずつ待機:まず名前を取得し、次に年齢を取得
  var name = await fetchUserName(id);
  print('名前を取得: $name');

  var age = await fetchUserAge(id);
  print('年齢を取得: $age');

  print('サンプル ユーザー: $name, 年齢: $age');
}

// 複数の非同期操作を並行して実行
Future<void> printUserInfoParallel(int id) async {
  print('ユーザー情報の並行取得を開始...');

  // 2つのリクエストを同時に開始し、互いに待たない
  var nameFuture = fetchUserName(id);
  var ageFuture = fetchUserAge(id);

  // 2つのリクエストが両方完了するのを待つ
  var results = await Future.wait([nameFuture, ageFuture]);
  var name = results[0] as String;
  var age = results[1] as int;

  print('サンプル ユーザー(並行): $name, 年齢: $age');
}

void main() async {
  // 順次実行(合計時間 ≈ 1s + 0.5s = 1.5s)
  await printUserInfo(1);

  print('---');

  // 並行実行(合計時間 ≈ max(1s, 0.5s) = 1s)
  await printUserInfoParallel(2);
}
开始获取用户信息...
获取到名称: 用户1
获取到年龄: 21
サンプル 用户: 用户1, 年龄: 21
---
开始并发获取用户信息...
サンプル 用户(并发): 用户2, 年龄: 22

複数の非同期操作の間に依存関係がない場合は、個別に await するのではなく、Future.wait を使って並行実行させるべきです。これによりパフォーマンスが大幅に向上します——合計所要時間は最も遅い操作と同じになり、全操作の合計ではありません。

async/await の主要ルール

ルール説明
async 関数は Future を返す関数が T を返す場合でも、async にすると自動的に Future<T> にラップされる
await は async 関数内でのみ使用できる通常の関数では await は使えない
await は現在の関数を一時停止するただし、他のコードの実行はブロックしない
await は Future の結果を取得するFuture にエラーがある場合、await は例外を投げる

then / catchError チェーン呼び出し

async/await に加えて、Dart は then() と catchError() を使ったチェーン呼び出しもサポートしています。

これは伝統的な Promise スタイルの書き方です。

実例

// ネットワークリクエストをシミュレートする関数
Future<String> fetchData(String url) {
  return Future.delayed(Duration(seconds: 1), () {
    if (url.isEmpty) {
      throw Exception('URL を空にすることはできません');
    }
    return '$url からのデータ';
  });
}

void main() {
  print('リクエスト開始...');

  // then / catchError チェーン呼び出し
  fetchData('https://example.com/api')
      .then((data) {
        // リクエスト成功
        print('データを取得: $data');
        return '処理後: $data';  // 戻り値は次の then に渡される
      })
      .then((processed) {
        // 前の then の戻り値を処理する
        print(processed);
      })
      .catchError((error) {
        // チェーン内のすべてのエラーを一括でキャッチ
        print('リクエストエラー: $error');
      })
      .whenComplete(() {
        // 成功でも失敗でも実行される(finally に似ている)
        print('リクエスト終了(成功でも失敗でも)');
      });

  print('リクエストは送信済み、プログラムは続行...');
}
开始请求...
请求已发出,程序继续...
获取到数据: 来自 https://example.com/api 的数据
处理后的: 来自 https://example.com/api 的数据
请求结束(无论成功或失败)

async/await vs then/catchError の選び方

シナリオ推奨される方法理由
線形の非同期フローasync/awaitコードが同期のように分かりやすい
複数のリクエストの並行async/await + Future.wait意味が明確
チェーンによるデータ変換then()各 then で1ステップずつ変換するため、チェーンが明確
単一のコールバックthen()async/await より簡潔

ほとんどの場合は async/await が推奨されます。可読性が高いためです。しかし、then() は単純なチェーン処理では依然として価値があります。両者は混在させることができますが、推奨されません。1つの関数では async/await か then/catchError のどちらかに統一すべきであり、混在させるとコードが理解しにくくなります。


非同期エラーの処理

非同期操作ではエラーが発生する可能性があるため、try-catch で処理する必要があります。

async/await はエラー処理を同期コードと同じくらいシンプルにします。

実例

// 失敗する可能性のあるネットワークリクエストをシミュレート
Future<String> fetchUserProfile(int userId) async {
  await Future.delayed(Duration(seconds: 1));

  if (userId <= 0) {
    throw Exception('無効なユーザー ID: $userId');
  }

  if (userId == 404) {
    throw HttpException('ユーザーは存在しません');
  }

  return ユーザー $userId のプロフィール;
}

// カスタム例外
class HttpException implements Exception {
  final String message;
  HttpException(this.message);

  @override
  String toString() => 'HttpException: $message';
}

Future<void> loadUserProfile(int userId) async {
  print(ユーザー $userId のプロフィールを読み込み中...);

  try {
    var profile = await fetchUserProfile(userId);
    print(読み込み成功: $profile);
  } on HttpException catch (e) {
    // タイプ別に特定の非同期エラーをキャッチ
    print(HTTP エラー: $e);
    print(ユーザーに注意喚起:ユーザーが存在するか確認してください);
  } on Exception catch (e) {
    // その他の例外をキャッチ
    print(一般エラー: $e);
  } finally {
    print(読み込み操作が終了しました);
  }
}

void main() async {
  await loadUserProfile(1);    // 成功
  print('---');
  await loadUserProfile(404);  // HttpException
  print('---');
  await loadUserProfile(-1);   // 一般 Exception
}
正在加载用户 1 的资料...
加载成功: 用户 1 的个人资料
加载操作结束
---
正在加载用户 404 的资料...
HTTP 错误: HttpException: 用户不存在
提示用户:请检查用户是否存在
加载操作结束
---
正在加载用户 -1 的资料...
一般错误: Exception: 无效的用户 ID: -1
加载操作结束

async 関数におけるエラー処理のルール:

  • async 関数内で例外がスローされた場合、その例外は自動的に Future にラップされます
  • 呼び出し側は try-catch で await の例外をキャッチできます
  • then() を使用する場合、例外は catchError() に渡されます
  • キャッチされていない非同期例外はプログラムをクラッシュさせません、ただし未処理の Future エラーと見なされます

非同期エラーを決して無視してはいけません。すべての async 関数呼び出しには対応するエラー処理が必要です。未処理の非同期エラーは開発モードでは警告が出力されますが、本番環境では黙って無視される可能性があり、調査が困難なバグの原因になります。

その他の拡張