Dart 非同期プログラミング — Future / async-await
非同期プログラミングはプログラムを待たせない — 3 秒の I/O 操作で CPU はただ待つのではなく他の作業ができる。
1. 学べること
- Future ライフサイクル:pending / completed / failed
- 非同期 / await シンタックスシュガーとエラー処理
- Future 合成:Future.wait / Future.any / Future.forEach
- Completer:Future の完了を手動制御
- Bob のシナリオ:DataPipeline による複数の EC API への並行リクエスト
2. 開発者のリアルな物語
(1) 課題:シーケンシャルリクエストによる 12 秒のページロード
Bob の DataPipeline は 3 つの API からデータを取得する必要がある:orders(3 秒)、products(2 秒)、customers(2 秒)。彼は当初シーケンシャルリクエストを使った:3 + 2 + 2 = 7 秒。データベースクエリ(5 秒)を加えると、レポート生成全体で 12 秒かかった。ユーザー体験は悲惨で、SaaS 顧客から「遅すぎる」との苦情が寄せられた。
(2) 解決策:Future.wait による並行実行
Future.wait を使うと 3 つの API リクエストが並行実行され、合計時間は最も遅いもので決まる:3 秒。並行データベースクエリと組み合わせると、レポート生成全体が 4 秒に短縮される。
// シーケンシャル:3 + 2 + 2 = 7 秒
final orders = await fetchOrders(); // 3秒
final products = await fetchProducts(); // 2秒
final customers = await fetchCustomers(); // 2秒
// 並行:max(3, 2, 2) = 3 秒
final results = await Future.wait([
fetchOrders(),
fetchProducts(),
fetchCustomers(),
]);
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- Future.wait の並行実行により API 呼び出しが 7 秒から 3 秒に短縮、全体で 4 秒
- 非同期/await により非同期コードが同期コードのように見え、可読性が向上
- 堅牢なエラー処理によりネットワーク例外でプログラムがクラッシュすることがなくなる
3. Future の基礎
(1) Future ライフサイクル
sequenceDiagram participant Bob participant API1 participant API2 participant API3 Bob->>API1: fetchOrders() Bob->>API2: fetchProducts() Bob->>API3: fetchCustomers() Note over Bob: Future.wait 並行 API1-->>Bob: 120 万件の注文 API2-->>Bob: 50 万件の商品 API3-->>Bob: 30 万件の顧客 Bob->>Bob: マージ&分析
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:Future の作成と使用
import 'dart:async';
// 遅延後に完了する Future を作成
Future<String> fetchOrder() async {
await Future.delayed(const Duration(seconds: 2));
return 'ORD-001: \$1500.00 USD';
}
// エラーを伴う Future
Future<String> fetchOrderWithRetry() {
return Future.delayed(const Duration(seconds: 1), () {
throw Exception('Network timeout');
});
}
void main() async {
// Pending → Completed
print('Fetching order...');
final order = await fetchOrder();
print(order);
// Pending → Failed
try {
await fetchOrderWithRetry();
} catch (e) {
print('Error: $e');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| Future 状態 | 意味 |
|---|---|
| pending | 未完了、待機中 |
| completed with 値 | 値で成功完了 |
| completed with エラー | 失敗完了、例外を伴う |
4. 非同期 / await 詳解
(1) 基本構文
▶ サンプル:非同期/await の基礎
import 'dart:async';
Future<double> calculateOrderTotal(String orderId) async {
// await は Future が完了するまで実行を一時停止
final amount = await fetchAmount(orderId);
final taxRate = await fetchTaxRate(orderId);
return amount * (1 + taxRate);
}
Future<double> fetchAmount(String orderId) async {
await Future.delayed(const Duration(milliseconds: 500));
return 1500.0;
}
Future<double> fetchTaxRate(String orderId) async {
await Future.delayed(const Duration(milliseconds: 300));
return 0.08;
}
void main() async {
final total = await calculateOrderTotal('ORD-001');
print('Total: \$${total.toStringAsFixed(2)} USD');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(2) エラー処理
▶ サンプル:非同期/await エラー処理
import 'dart:async';
Future<String> fetchApiData(String endpoint) async {
await Future.delayed(const Duration(seconds: 1));
if (endpoint.contains('invalid')) {
throw Exception('API error: $endpoint not found');
}
return 'Data from $endpoint';
}
Future<void> robustFetch(String endpoint) async {
try {
final data = await fetchApiData(endpoint);
print('Success: $data');
} on Exception catch (e) {
print('Exception: $e');
} finally {
print('Fetch completed for $endpoint');
}
}
void main() async {
await robustFetch('orders'); // Success
await robustFetch('invalid-endpoint'); // Exception
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| エラー処理方法 | 構文 | ユースケース |
|---|---|---|
| try-catch | try { await f(); } catch (e) {} |
回復が必要な場合 |
| catchError | f().catchError((e) => ...) |
関数型スタイル |
| onError | f().then(..., onError: ...) |
シンプルなコールバック |
5. Future 合成
(1) Future.wait — 並行待機
▶ サンプル:Future.wait 並行
import 'dart:async';
Future<List<String>> fetchOrders() async {
await Future.delayed(const Duration(seconds: 3));
return ['ORD-001', 'ORD-002', 'ORD-003'];
}
Future<List<String>> fetchProducts() async {
await Future.delayed(const Duration(seconds: 2));
return ['Laptop', 'Mouse', 'Keyboard'];
}
Future<List<String>> fetchCustomers() async {
await Future.delayed(const Duration(seconds: 2));
return ['Alice', 'Bob', 'Charlie'];
}
void main() async {
// 並行:max(3, 2, 2) = 約3秒
final stopwatch = Stopwatch()..start();
final results = await Future.wait([
fetchOrders(),
fetchProducts(),
fetchCustomers(),
]);
stopwatch.stop();
print('Orders: ${results[0]}');
print('Products: ${results[1]}');
print('Customers: ${results[2]}');
print('Time: ${stopwatch.elapsedMilliseconds}ms');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:Future.wait エラー処理
import 'dart:async';
Future<String> riskyFetch(String name, bool shouldFail) async {
await Future.delayed(const Duration(seconds: 1));
if (shouldFail) throw Exception('$name failed');
return '$name data';
}
void main() async {
// Future.wait は早期失敗 - いずれかが失敗すると全体が失敗
try {
await Future.wait([
riskyFetch('API-1', false),
riskyFetch('API-2', true), // これが失敗する
riskyFetch('API-3', false),
]);
} catch (e) {
print('Future.wait failed: $e');
}
// eagerError: false で個別結果を保持
final results = await Future.wait(
[
riskyFetch('API-1', false).then((v) => Result.success(v)),
riskyFetch('API-2', true).then((v) => Result.success(v)).catchError((e) => Result.failure(e.toString())),
riskyFetch('API-3', false).then((v) => Result.success(v)),
],
);
for (final r in results) {
print(r.isSuccess ? 'OK: ${r.data}' : 'FAIL: ${r.error}');
}
}
class Result<T> {
final T? data;
final String? error;
final bool isSuccess;
Result.success(this.data) : error = null, isSuccess = true;
Result.failure(this.error) : data = null, isSuccess = false;
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(2) Future.any — レース
▶ サンプル:Future.any レース
import 'dart:async';
Future<String> fetchFromCache() async {
await Future.delayed(const Duration(milliseconds: 100));
return 'Cache: ORD-001 data';
}
Future<String> fetchFromApi() async {
await Future.delayed(const Duration(seconds: 2));
return 'API: ORD-001 data';
}
Future<String> fetchFromDb() async {
await Future.delayed(const Duration(milliseconds: 500));
return 'DB: ORD-001 data';
}
void main() async {
// 最初に完了したものを返す
final fastest = await Future.any([
fetchFromCache(),
fetchFromApi(),
fetchFromDb(),
]);
print('Fastest: $fastest'); // Cache: ORD-001 data
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) Future.forEach — シーケンシャル反復
▶ サンプル:Future.forEach
import 'dart:async';
Future<void> processOrder(String orderId) async {
await Future.delayed(const Duration(milliseconds: 500));
print('Processed: $orderId');
}
void main() async {
final orders = ['ORD-001', 'ORD-002', 'ORD-003'];
// シーケンシャル処理
await Future.forEach(orders, processOrder);
print('All orders processed');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 合成方法 | 動作 | 並行性 | ユースケース |
|---|---|---|---|
Future.wait |
すべてを待機 | 並行 | バッチリクエスト |
Future.any |
最初に完了したもの | 並行 | レース / フォールバック |
Future.forEach |
シーケンシャル完了 | シーケンシャル | 順序依存の操作 |
6. Completer 手動制御
▶ サンプル:Completer
import 'dart:async';
// Completer による手動 Future 制御
class ApiService {
final Completer<String> _initCompleter = Completer<String>();
Future<String> get initialized => _initCompleter.future;
void onConnected(String serverInfo) {
if (!_initCompleter.isCompleted) {
_initCompleter.complete(serverInfo);
}
}
void onError(Object error) {
if (!_initCompleter.isCompleted) {
_initCompleter.completeError(error);
}
}
}
void main() async {
final service = ApiService();
// 非同期初期化をシミュレート
Future.delayed(const Duration(seconds: 1), () {
service.onConnected('Server v3.0.1, 1,000,000 records');
});
print('Waiting for initialization...');
final info = await service.initialized;
print('Connected: $info');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
7. Bob のシナリオ:並行 API リクエスト
▶ サンプル:DataPipeline 並行データ取得
import 'dart:async';
// API 呼び出しをシミュレート
Future<List<Map<String, dynamic>>> fetchOrders() async {
await Future.delayed(const Duration(seconds: 2));
return [
{'id': 'ORD-001', 'amount': 1500.0, 'category': 'Electronics'},
{'id': 'ORD-002', 'amount': 3200.0, 'category': 'Electronics'},
{'id': 'ORD-003', 'amount': 890.0, 'category': 'Clothing'},
];
}
Future<Map<String, double>> fetchProductPrices() async {
await Future.delayed(const Duration(seconds: 1));
return {'Laptop': 1299.99, 'Mouse': 29.99, 'Keyboard': 79.99};
}
Future<List<String>> fetchCustomerNames() async {
await Future.delayed(const Duration(seconds: 1));
return ['Alice', 'Bob', 'Charlie'];
}
// エラー処理付きの並行データ取得
Future<void> generateReport() async {
final stopwatch = Stopwatch()..start();
try {
// 全データを並行取得
final results = await Future.wait([
fetchOrders().then((v) => ('orders', v)).catchError((e) => ('orders', null)),
fetchProductPrices().then((v) => ('products', v)).catchError((e) => ('products', null)),
fetchCustomerNames().then((v) => ('customers', v)).catchError((e) => ('customers', null)),
]);
stopwatch.stop();
print('=== DataPipeline Report ===');
print('Fetch time: ${stopwatch.elapsedMilliseconds}ms');
for (final (key, value) in results) {
if (value != null) {
print('$key: OK (${value is List ? value.length : value.length} items)');
} else {
print('$key: FAILED');
}
}
} catch (e) {
print('Report generation failed: $e');
}
}
void main() async {
await generateReport();
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
8. 完全なサンプル:DataPipeline 非同期データ処理
// ============================================
// DataPipeline 非同期データ処理
// Future、async/await、並行操作
// ============================================
import 'dart:async';
// シミュレートされたデータソース
Future<List<Map<String, dynamic>>> fetchOrders({int delay = 2}) async {
await Future.delayed(Duration(seconds: delay));
return [
{'id': 'ORD-001', 'amount': 1500.0, 'status': 'completed', 'category': 'Electronics'},
{'id': 'ORD-002', 'amount': 50.0, 'status': 'completed', 'category': 'Books'},
{'id': 'ORD-003', 'amount': 3200.0, 'status': 'pending', 'category': 'Electronics'},
{'id': 'ORD-004', 'amount': 890.0, 'status': 'completed', 'category': 'Clothing'},
];
}
Future<List<Map<String, dynamic>>> fetchProducts({int delay = 1}) async {
await Future.delayed(Duration(seconds: delay));
return [
{'name': 'Laptop', 'price': 1299.99, 'category': 'Electronics'},
{'name': 'Mouse', 'price': 29.99, 'category': 'Electronics'},
{'name': 'Novel', 'price': 12.99, 'category': 'Books'},
];
}
Future<double> fetchTaxRate(String region) async {
await Future.delayed(const Duration(milliseconds: 500));
return switch (region) {
'US' => 0.08,
'EU' => 0.20,
'UK' => 0.15,
_ => 0.10,
};
}
// 並行操作を含む非同期パイプライン
class AsyncPipeline {
final String name;
String status = 'idle';
AsyncPipeline({required this.name});
Future<Map<String, dynamic>> run({String region = 'US'}) async {
status = 'running';
final stopwatch = Stopwatch()..start();
try {
// ステップ 1: 並行データ取得
final (orders, products, taxRate) = await (
fetchOrders(),
fetchProducts(),
fetchTaxRate(region),
).wait;
// ステップ 2: 注文を処理
final completedOrders = orders
.where((o) => o['status'] == 'completed')
.toList();
// ステップ 3: 売上を計算
final revenue = completedOrders.fold<double>(
0, (sum, o) => sum + (o['amount'] as double));
final taxAmount = revenue * taxRate;
final totalWithTax = revenue + taxAmount;
// ステップ 4: カテゴリ別にグループ化
final byCategory = <String, double>{};
for (final order in completedOrders) {
final cat = order['category'] as String;
final amt = order['amount'] as double;
byCategory.update(cat, (v) => v + amt, ifAbsent: () => amt);
}
stopwatch.stop();
status = 'completed';
return {
'pipeline': name,
'region': region,
'taxRate': taxRate,
'totalOrders': orders.length,
'completedOrders': completedOrders.length,
'revenue': revenue,
'tax': taxAmount,
'totalWithTax': totalWithTax,
'byCategory': byCategory,
'processingTime': stopwatch.elapsedMilliseconds,
'status': status,
};
} catch (e) {
status = 'failed';
rethrow;
}
}
}
void main() async {
final pipeline = AsyncPipeline(name: 'E-Commerce Analytics');
print('=== Starting DataPipeline ===');
final report = await pipeline.run(region: 'US');
print('\n=== Report ===');
print('Pipeline: ${report['pipeline']}');
print('Region: ${report['region']}');
print('Tax Rate: ${(report['taxRate'] as double * 100).toStringAsFixed(1)}%');
print('Total Orders: ${report['totalOrders']}');
print('Completed: ${report['completedOrders']}');
print('Revenue: \$${(report['revenue'] as double).toStringAsFixed(2)} USD');
print('Tax: \$${(report['tax'] as double).toStringAsFixed(2)} USD');
print('Total: \$${(report['totalWithTax'] as double).toStringAsFixed(2)} USD');
print('Time: ${report['processingTime']}ms');
print('\nBy Category:');
for (final entry in (report['byCategory'] as Map<String, double>).entries) {
print(' ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== Starting DataPipeline ===
=== Report ===
Pipeline: E-Commerce Analytics
Region: US
Tax Rate: 8.0%
Total Orders: 4
Completed: 3
Revenue: $2440.00 USD
Tax: $195.20 USD
Total: $2635.20 USD
Time: 2000ms
By Category:
Electronics: $1500.00 USD
Books: $50.00 USD
Clothing: $890.00 USD
❓ よくある質問
Q: 非同期 関数の戻り値型は何ですか? A: 非同期 関数は戻り値を自動的に Future にラップします。
Future<T>戻り型を宣言し、関数内では単純に T を返してください。
Q: await は 非同期 関数内でのみ使えますか? A: はい。await は 非同期 関数または 非同期* ジェネレーター内でのみ使用できます。Dart 3.5+ はトップレベル await をサポートします(Dart スクリプトのみ)。
Q: Future.wait で Future が失敗した場合はどうなりますか? A: デフォルトでは、いずれかの Future が失敗すると Future.wait は即座にその例外をスローします(他の Future は実行を続けますが、結果は無視されます)。
eagerError: falseを使うとすべてが完了するまで待機できます。
Q: 非同期/await と .then() の違いは何ですか? A: 機能的には同等ですが、非同期/await の方が読みやすく(線形フロー)、.then() の方が関数型的(チェーン)です。日常的な使用には 非同期/await を推奨します。.then() はシンプルな変換に適しています。
Q: いつ Completer を使うべきですか? A: Future の完了タイミングを手動で制御する必要があるときに使ってください。一般的なシナリオ:イベントコールバックから Future への変換、WebSocket メッセージ、サードパーティライブラリのコールバック。ほとんどの場合、非同期/await を使えば十分です。
Q: Future はマルチキャストですか、ユニキャストですか? A: Future はユニキャストです — 1 つの await または .then() でのみ消費できます。マルチキャストには Stream または asStream() が必要です。
Q: タイムアウト制御を実装するには? A: Future.timeout() メソッドを使います。例:
await fetchApi().timeout(Duration(seconds: 5), onTimeout: () => defaultData)。
📖 まとめ
- Future は非同期計算の最終結果を表し、3 つの状態(pending/completed/failed)を持つ。
- 非同期/await は Future のシンタックスシュガーであり、非同期コードを同期コードのように読めるようにする。
- Future.wait は複数の Future を並行実行し、合計時間は最も遅いもので決まる。
- Future.any は最も速いものを取得するためにレースする;Future.forEach はシーケンシャルに実行する。
- Completer は Future の完了を手動制御し、コールバックを Future に変換するのに適している。
📝 練習問題
- 基礎(難易度 ⭐):それぞれ 1/2/3 秒の遅延を持つ Future を返す 3 つの関数を書いてください。非同期/await を使ってシーケンシャルに呼び出し、合計時間を測定します。その後、Future.wait を使って並行に呼び出し、時間を比較します。
- 中級(難易度 ⭐⭐):リトライ付き非同期リクエスト関数を実装してください:
fetchWithRetry(url, retries: 3, backoff: Duration)。失敗時に指数バックオフでリトライし、リトライ回数を超えた後に例外をスローします。 - 挑戦(難易度 ⭐⭐⭐):Completer を使ってシンプルな「リクエスト - レスポンス」プロトコルを実装してください:リクエストを送信し、対応する応答を待つ、タイムアウトサポート付き。複数のリクエストを並行送信できるメッセージバスをシミュレートします。