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 の作成と使用:
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 の完了を待ってその結果を取得します。
実例
// 非同期関数の戻り値の型は 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 関数呼び出しには対応するエラー処理が必要です。未処理の非同期エラーは開発モードでは警告が出力されますが、本番環境では黙って無視される可能性があり、調査が困難なバグの原因になります。