FlutterとDartでレスポンシブで高速なアプリケーションを構築する上で、非同期プログラミングの理解と活用は不可欠です。非同期処理を使いこなすことで、インターネットからのデータ取得やファイル読み込みといった時間のかかるタスクを実行している間も、ユーザーインターフェースが固まる(フリーズする)のを防ぐことができます。
Dartの非同期モデルの中心には、FutureとStreamという2つの強力な概念があります。そして、これらを扱うために必須となるのがasync
とasync*
という2つのキーワードです。この記事では、それぞれの役割と適切な使い分けについて、具体例を交えながら詳しく解説します。
FutureとStreamとは?
キーワードの解説に入る前に、それらが生成するオブジェクトについて簡単におさらいしましょう。
Future
: 将来のある時点で利用可能になる「単一の」値またはエラーを表します。非同期操作から返される「1回限りの結果」の約束手形のようなものです。例えば、HTTPリクエストは最終的に1つのレスポンスを返します。Stream
: 非同期イベントのシーケンス(流れ)です。単一の結果ではなく、時間をかけて複数の値を生成できます。WebSocket接続からのデータストリームや、ユーザーの入力イベントなどを想像すると分かりやすいでしょう。
`async` vs `async*`: 中核となる違い
どちらのキーワードも非同期関数を定義するために使われますが、その目的は根本的に異なります。主な違いは、何を返し、どのように値を生成するかにあります。
特徴 | async |
async* |
---|---|---|
戻り値の型 | Future |
Stream |
値の数 | 1つ(またはエラー) | 0個以上 |
値の生成方法 | return キーワードを使用 |
yield キーワードを使用 |
主な用途 | API呼び出しやファイルI/Oなど、1回限りの非同期タスク。 | リアルタイム更新やイベントリスナーなど、継続的なデータフローの処理。 |
`async`と`Future`を深く知る
async
キーワードは、関数を非同期としてマークするために使用します。この修飾子により、関数は即座にFuture
オブジェクトを返します。関数の本体は後で実行され、完了するとそのFutureを値またはエラーで解決します。
実践例: ネットワークデータの取得
async
の典型的なユースケースは、Webサーバーからデータを取得することです。この操作は時間がかかり、メインスレッドをブロックすべきではありません。
import 'dart:convert';
import 'package:http/http.dart' as http;
// この関数は、最終的にStringを格納するFutureを返します。
Future<String> fetchUserData() async {
try {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));
if (response.statusCode == 200) {
// サーバーが200 OKを返した場合、JSONを解析します。
return jsonDecode(response.body)['title'];
} else {
// サーバーが200 OK以外を返した場合、例外をスローします。
throw Exception('ユーザーデータの読み込みに失敗しました');
}
} catch (e) {
return 'データ取得エラー: $e';
}
}
async
関数内では、await
キーワードを使用して、別の非同期操作(別のFuture
)が完了するまで関数の実行を一時停止できます。
Futureの利用方法
fetchUserData
から返されたFuture
から値を取得するには、主に2つの方法があります。
void main() async {
// 方法1: awaitを使用(別のasync関数内)
print('ユーザーデータを取得中...');
String data = await fetchUserData();
print('受信データ: $data');
// 方法2: .then()を使用
fetchUserData().then((value) {
print('.then()で受信したデータ: $value');
}).catchError((error) {
print('キャッチしたエラー: $error');
});
}
`async*`と`Stream`を深く知る
async*
(「エイシンク・スター」と読みます)キーワードは、Stream
を返す関数を定義するために使用します。この種の関数は、時間をかけて一連の値を生成できるため、「ジェネレータ関数」として知られています。
実践例: カウントダウンストリームの作成
カウントダウンタイマーのように、毎秒数値を生成する関数が必要だとします。これはasync*
とStream
にとって完璧な仕事です。
// この関数は、毎秒整数を生成するStreamを返します。
Stream<int> countdown(int from) async* {
for (int i = from; i >= 0; i--) {
// 1秒待機します。
await Future.delayed(Duration(seconds: 1));
// 'yield'はストリームに値を送り出します。
yield i;
}
}
async*
関数はreturn
の代わりにyield
キーワードを使って値を送り出します。関数の実行は各yield
で一時停止し、ストリームの利用者が次の値を要求すると再開します。
Streamの利用方法
Stream
から出力される値は、await for
ループやlisten()
メソッドを使って受け取ることができます。
void main() async {
print('カウントダウンを開始します...');
Stream numberStream = countdown(5);
// 方法1: await forを使用(シンプルで推奨)
await for (int number in numberStream) {
print(number);
}
print('カウントダウン終了!');
// 方法2: .listen()を使用
// 注:ストリームは一度しかリッスンできません。
// これを使用するには、再度countdown(3)を呼び出す必要があります。
countdown(3).listen(
(number) {
print('Listen: $number');
},
onDone: () {
print('Listen: カウントダウン終了!');
},
);
}
`async`と`async*`の組み合わせ: 高度なシナリオ
これらの概念を組み合わせることで、強力なデータ処理パイプラインを構築できます。例えば、IDのストリームを受け取り、各IDに対して非同期でデータを検索するようなケースです。
この例では、先ほどのcountdown
ストリームでIDを生成し、fetchUserData
のロジックを使って各IDを処理します。
// 特定のIDでTodo項目を取得する関数
Future<String> fetchTodoById(int id) async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/$id'));
if (response.statusCode == 200) {
return jsonDecode(response.body)['title'];
} else {
throw Exception('Todo #$id の読み込みに失敗しました');
}
}
// IDのストリームを受け取り、取得したデータをyieldで返す関数
Stream<String> fetchTodosFromStream(Stream<int> idStream) async* {
await for (final id in idStream) {
try {
// ストリームからの各IDに対して、async関数を呼び出す
String todoTitle = await fetchTodoById(id);
// 結果を出力ストリームにyieldする
yield 'Todo #$id: $todoTitle';
} catch (e) {
yield 'Todo #$id の取得エラー: $e';
}
}
}
void main() async {
// 1, 2, 3という数値を生成するストリームを作成
Stream<int> idStream = Stream.fromIterable([1, 2, 3]);
// IDのストリームを処理
await for (String result in fetchTodosFromStream(idStream)) {
print(result);
}
}
ここで、fetchTodosFromStream
関数は結果のStream
を生成するためasync*
でマークされています。その内部では、await for
でIDの入力ストリームを処理し、各IDに対してasync
関数であるfetchTodoById
をawait
で呼び出しています。
結論: 重要なポイント
async
とasync*
の違いを理解することは、効率的でクリーンな非同期Dartコードを書く上で非常に重要です。
async
/Future
: API呼び出しなど、単一の結果を生成する操作に使用します。関数は一度だけ値を返します。async*
/Stream
: リアルタイムのデータフィードやイベント処理など、時間をかけて一連の値を生成する操作に使用します。関数は複数回値をyield
できます。
これらのツールをマスターすることで、Flutterアプリケーションにおけるあらゆる非同期の課題に対応し、スムーズで応答性の高いユーザー体験を保証することができます。
0 개의 댓글:
Post a Comment