【Flutter/Dart】Futureで非同期処理の実装方法!await/asyncの使い方
この記事からわかること
- Flutter/DartでFutureで非同期処理を実装する方法
- await/asyncの使い方
- thenやwaitメソッドなどの使い方
- FutureBuilderでUIに反映させる方法
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Meerkat
- Xcode:16.0
- Flutter:3.29.2
- Dart:3.7.2
- Mac M1:Sequoia 15.4
公式リファレンス:Asynchronous programming: Streams
Dartでの非同期処理
アプリ開発をしていく中で非同期処理の実装はマストになってきます。「非同期処理」とは処理の結果を待たずに並行して別の処理を進めるような処理のことです。例えばネットワーク通信やデータベースのCRUD操作、ファイルの読み書きなど処理に時間のかかる操作はアプリが一時的に停止してしまう原因になります。これらの処理を非同期で実行できるようにすることで、ユーザーはアプリをスムーズに操作し続けることができます。
反対の言葉で「同期処理」があります。これは処理が完了するまで次の処理には進まず、すべてを順番通りに実行する処理方法です。時間のかかる処理を同期的に行うと、アプリの応答が止まってしまいUXが低下してしまいます。
Dartにおける非同期プログラミングはFutureクラスとStreamクラス、async/awaitキーワードなどによって制御することが可能になっています。
- Future・・・1回だけの非同期結果
- Stream・・・複数の非同期イベント(値・エラー・完了)
Futureクラス
Futureは未来(future)に取得できる非同期処理の結果を管理するためのオブジェクトです。使い方は関数の返り値をFuture<任意のデータ型>で定義します。例えば以下は非同期にしたい処理(時間のかかる処理)ではなく同期的に実行しても問題ない処理を敢えてFutureにしてみた場合でこの場合はFuture.value()メソッドでレスポンスを行います。
/// 時間のかからない処理
Future<String> fetchDataNotAsync() {
return Future.value('データ取得完了');
}
時間のかかる処理の場合はasync/awaitキーワードを使用します。例えば擬似的に再現するためにFuture.delayedを使用し遅延させてみます。asyncはメソッド自体にawaitは時間のかかる処理に記述します。どれでもつけれるわけではなく戻り値がFutureで定義されているメソッドで使用します。この場合はFuture<任意のデータ型>で定義しているデータ型をそのまま返却します。
// 時間のかかる処理
Future<String> fetchData() async {
// 疑似的に2秒待機
await Future.delayed(Duration(seconds: 2));
return 'データ取得完了';
}
定義したfetchDataを呼び出すときも呼び出し側にasyncを付与し、awaitを使用して呼び出します。
void main() async {
print('START');
// 結果を取得できるまで待機
String result = await fetchData();
print(result);
print('END');
}
// 出力
START
データ取得完了 // 2秒後
END
エラーハンドリング
Futureではエラーをスローすることができます。エラーをスローする方法はthrowで直接エラーを投げる方法とFuture.error()を使用する2パターンあります。
Future<String> fetchDataError() async {
throw Exception('データ取得に失敗しました');
// または以下
// return Future.error('不明なエラー');
}
呼び出し側でエラーハンドリングを行うためにはtry ~ catch文を使用してエラーをキャッチします。
void main() async {
print('START');
try {
String result = await fetchDataError();
// エラーになると以下は呼ばれない
print(result);
} catch (e) {
print('エラー発生: $e');
}
print('END');
}
thenメソッド
非同期処理をawaitを使わずに結果後の処理をコールバックで記述できるのがthenメソッドです。クロージャーから結果を取得でき、処理を記述することができ、呼び出し側でもasyncの付与が不要になります。
void main() {
print('START');
fetchData().then((value) {
print('Success: $value');
});
print('END');
}
// 出力
START
END
Success: データ取得完了 // 2秒後
thenはメソッドチェーンで処理を連結させることも可能です。
Future<int> calc() {
return Future.delayed(Duration(seconds: 1), () => 10);
}
void main() {
calc()
.then((value) => value * 2)
.then((result) => print('結果: $result')); // 結果: 20
}
catchErrorメソッド
thenで結果を取得する際のエラーハンドリングはcatchErrorメソッドで行います。
Future<String> fetchError() {
return Future.error('エラー発生');
}
void main() {
fetchError()
.then((value) => print('成功: $value'))
.catchError((e) => print('失敗: $e'));
}
waitメソッド
waitメソッドは複数の非同期処理(返り値がFuture)を並列で実行し、すべての結果を待機して取得することができます。処理は並列に実行されるため、最も遅い非同期処理が完了したタイミングで結果を取得することができます。
void main() async {
final results = await Future.wait([
// 遅延処理1
Future.delayed(Duration(seconds: 1), () => 'A'),
// 遅延処理2
Future.delayed(Duration(seconds: 4), () => 'B'),
]);
// 上記の場合4秒後に以下の結果を取得できる
print(results); // ['A', 'B']
}
waitの引数にはIterable<Future>型を返す処理を指定することができるるのでListやSetなどを使用することが可能です。
forEachメソッド
forEachメソッドは指定したコレクション要素の中身に対して非同期処理を繰り返し行うことができます。コレクションはIterable<T>型で指定することができ、クロージャーの引数で各要素を順番に参照することができます。
void main() async {
await Future.forEach([1, 2, 3], (num) async {
await Future.delayed(Duration(seconds: 1));
print('処理: $num');
});
}
delayedメソッド
delayedは非同期処理内で遅延処理を行うメソッドです。引数にはDuration型で遅延させたい時間を指定します。2つ目の引数には指定時間後に実行させたい処理を指定することもできます。
// 1秒遅延
await Future.delayed(Duration(seconds: 1));
// 500ミリ秒遅延
await Future.delayed(Duration(milliseconds: 500));
// 1秒遅延後に任意の処理を実行
final result = await Future.delayed(
Duration(seconds: 1),
() => '完了データ',
);
timeoutメソッド
timeoutは指定時間を超えたらタイムアウトさせるメソッドです。引数にはタイムアウトを設ける時間をDuration型で指定します。タイムアウトするとTimeoutExceptionがスローされます。
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 3));
return 'データ取得完了';
}
void main() async {
try {
final result = await fetchData().timeout(Duration(seconds: 2));
print(result);
} catch (e) {
// TimeoutException after 0:00:02.000000: Future not completed
print('タイムアウト: $e');
}
}
FutureBuilder
Future型はFutureBuilderを使用することでUIに簡単に組み込むことが可能になっています。例えば以下のような記事を取得してくるFutureのメソッドがあるとします。
/// 最新の記事を取得する
Future<List<QiitaItem>> fetchLatestItems(
{int page = 1, int perPage = 10}) async {
try {
final response = await apiService.get(
'/items?page=$page&per_page=$perPage');
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((e) => QiitaItem.fromJson(e)).toList();
} else {
throw Exception('通信失敗: ${response.statusCode}');
}
} catch (e) {
throw Exception('例外: $e');
}
}
これをFutureBuilderを使用することで以下のようにUIと連動させて実装することができます。
FutureBuilder<List<QiitaItem>>(
future: repository.fetchLatestItems(),
builder: (context, snapshot) {
// 通信中
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
// エラーが出たら表示
if (snapshot.hasError) {
return Center(child: Text('エラー: ${snapshot.error}'));
}
// データがない場合
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('データが見つかりません'));
}
final items = snapshot.data!;
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
subtitle: Text('投稿者: ${item.userId}'),
);
},
);
},
)
snapshotからは処理状態や結果、エラー内容などを取得することが可能です。
| プロパティ | 概要 |
|---|---|
| connectionState | 処理中・完了などの状態(waiting, done, etc..) |
| hasData | データが存在するか |
| data | データ本体 |
| hasError | エラーが存在するか |
| error | エラー内容 |
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





