【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 | エラー内容 |
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。