Wednesday, March 20, 2024

Flutter/Dart: `async` vs `async*` - A Practical Guide to Asynchronous Code

FlutterとDartでレスポンシブで高速なアプリケーションを構築する上で、非同期プログラミングの理解と活用は不可欠です。非同期処理を使いこなすことで、インターネットからのデータ取得やファイル読み込みといった時間のかかるタスクを実行している間も、ユーザーインターフェースが固まる(フリーズする)のを防ぐことができます。

Dartの非同期モデルの中心には、FutureStreamという2つの強力な概念があります。そして、これらを扱うために必須となるのがasyncasync*という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 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 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 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 fetchTodosFromStream(Stream 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 idStream = Stream.fromIterable([1, 2, 3]);

  // IDのストリームを処理
  await for (String result in fetchTodosFromStream(idStream)) {
    print(result);
  }
}

ここで、fetchTodosFromStream関数は結果のStreamを生成するためasync*でマークされています。その内部では、await forでIDの入力ストリームを処理し、各IDに対してasync関数であるfetchTodoByIdawaitで呼び出しています。

結論: 重要なポイント

asyncasync*の違いを理解することは、効率的でクリーンな非同期Dartコードを書く上で非常に重要です。

  • async / Future: API呼び出しなど、単一の結果を生成する操作に使用します。関数は一度だけ値を返します。
  • async* / Stream: リアルタイムのデータフィードやイベント処理など、時間をかけて一連の値を生成する操作に使用します。関数は複数回値をyieldできます。

これらのツールをマスターすることで、Flutterアプリケーションにおけるあらゆる非同期の課題に対応し、スムーズで応答性の高いユーザー体験を保証することができます。


0 개의 댓글:

Post a Comment