Wednesday, March 20, 2024

Flutterの非同期処理をマスターする:async、Isolate、Stream徹底解説

Googleが開発したオープンソースのUIツールキットであるFlutterは、単一のコードベースからモバイル、Web、デスクトップ向けの美しいネイティブアプリを構築できます。しかし、美しいUIは成功の半分に過ぎません。真に優れたユーザー体験を提供するためには、アプリは応答性が高く、スムーズで、高速でなければなりません。ここで活躍するのが、Flutterの強力な非同期プログラミング機能です。

このガイドでは、DartとFlutterにおける並行処理の3つの柱、すなわち遅延のある処理を扱うためのasync/await、真の並列処理を実現するIsolate、そして時間とともに流れてくる一連のデータを管理するStreamについて深く掘り下げます。これらの概念を理解することは、高性能なアプリケーションを開発し、UIの「カクつき」(画面の描画が止まったり、もたついたりする現象)を解消するための鍵となります。

中心的な課題:UIスレッドの保護

アプリのユーザーインターフェースは、1秒間に60回から120回画面を更新し続ける、たった一人の専任作業員だと想像してみてください。もしこの作業員に、インターネットからデータを取得したり、複雑な計算をしたりといった時間のかかるタスクを任せると、その間、画面の更新作業が止まってしまいます。その結果、アプリはフリーズし、応答不能に陥ります。非同期プログラミングとは、こうした時間のかかるタスクを他の作業員に委任し、UIスレッド(メインスレッド)が本来の仕事に集中できるようにするための戦略なのです。

1. `async` と `await`:待機を伴う処理のために

最も一般的な非同期タスクは、プログラムがネットワークやデータベースといった外部リソースを待つ必要があるI/Oバウンドな処理です。`async`と`await`キーワードは、このような状況をクリーンで読みやすいコードで扱うための仕組みを提供します。

これはカフェでコーヒーを注文するのに似ています。あなたは注文し(`await`の呼び出し)、レシート(`Future`オブジェクト)を受け取ります。バリスタをじっと見つめて待つのではなく、自由にスマートフォンをチェックしたり、友人と話したりできます(UIスレッドはブロックされません)。そして名前が呼ばれたら、コーヒーの準備が完了し(`Future`が完了)、あなたは次の行動に移れます。

主要な概念:

  • `Future`:将来のある時点で利用可能になる「値」または「エラー」を表すオブジェクト。
  • `async`:関数を非同期としてマークするキーワード。関数の戻り値を暗黙的に`Future`でラップします。
  • `await`:`Future`が完了するまで`async`関数の実行を一時停止するキーワード。`async`関数内でのみ使用できます。

実践的な例:ユーザーデータの取得

単に待機するだけでなく、APIからユーザーデータを取得する、より現実的なネットワークリクエストをシミュレートしてみましょう。ここでは、`Future`の結果に基づいてUIを構築するための一般的なFlutterのパターンである`FutureBuilder`ウィジェットを使用します。


import 'dart:convert';
import 'package:http/http.dart' as http;

// APIからユーザープロファイルを非同期で取得する関数
Future<String> fetchUserData() async {
  // 'await'は、ネットワーク呼び出しが完了するまでここで実行を一時停止します。
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users/1'));

  if (response.statusCode == 200) {
    // サーバーが200 OKを返した場合、JSONを解析します。
    String userName = jsonDecode(response.body)['name'];
    return 'ようこそ、$userName さん!';
  } else {
    // サーバーが200 OKを返さなかった場合、例外をスローします。
    throw Exception('ユーザーデータの読み込みに失敗しました');
  }
}

// Flutterウィジェット内での使用例:
// Widget build(BuildContext context) {
//   return FutureBuilder<String>(
//     future: fetchUserData(), // 監視対象のFuture
//     builder: (context, snapshot) {
//       if (snapshot.connectionState == ConnectionState.waiting) {
//         return CircularProgressIndicator(); // 待機中はローディングインジケーターを表示
//       } else if (snapshot.hasError) {
//         return Text('エラー: ${snapshot.error}'); // エラーメッセージを表示
//       } else if (snapshot.hasData) {
//         return Text(snapshot.data!); // データが到着したら表示
//       } else {
//         return Text('データがありません');
//       }
//     },
//   );
// }

2. `Isolate`:CPU負荷の高い重い処理のために

もしタスクがI/Oを待つのではなく、大きな画像の処理や巨大なJSONファイルの解析など、CPUパワーを激しく消費するものだったらどうでしょう?これをメインのUIスレッドで実行すると、深刻なフリーズを引き起こします。ここで`Isolate`の出番です。

IsolateはDartの並行処理モデルです。メモリを共有し、競合状態やデッドロックといった複雑な問題を引き起こす可能性のある従来のスレッドとは異なり、各Isolateはそれぞれ独自のメモリヒープを持ち、状態を一切共有しません。それらは完全に「隔離」されており、メッセージの受け渡しによってのみ通信します。これは、非常に騒がしく手間のかかる仕事を、防音設備の整った別の作業場にいる専門家に依頼し、自分は静かに作業を続けるようなものです。

簡単な方法:`compute`関数

`Isolate.spawn`を使って手動でIsolateを管理することもできますが、Flutterは`compute`というはるかにシンプルなヘルパー関数を提供しています。これは、指定した関数を新しいIsolateで実行し、引数を渡し、結果を`Future`で返してくれます。

実践的な例:巨大なJSONの解析

APIから非常に大きなJSON文字列を受け取ったとします。これをデコードする処理は、UIのカクつきを引き起こすほど遅くなる可能性があります。この作業を`compute`を使って別のIsolateにオフロードできます。


import 'dart:convert';
import 'package:flutter/foundation.dart';

// この関数が別のIsolateで実行されます。
// トップレベル関数またはstaticメソッドである必要があります。
List<Photo> parsePhotos(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
  return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}

// シンプルなデータクラス
class Photo {
  final int id;
  final String title;
  Photo({required this.id, required this.title});

  factory Photo.fromJson(Map<String, dynamic> json) {
    return Photo(id: json['id'], title: json['title']);
  }
}

// メインコードからの呼び出し方
Future<List<Photo>> fetchAndParsePhotos() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
  
  // 'compute'関数を使い、'parsePhotos'を別のIsolateで実行します。
  // これにより、巨大なJSONレスポンスを解析している間もUIがフリーズするのを防ぎます。
  return compute(parsePhotos, response.body);
}

3. `Stream`:連続する非同期イベントのために

`Future`が単一の値を返すのに対し、`Stream`は時間とともに一連の値(またはエラー)を配信します。`Future`を一度きりの配達、`Stream`を定期購読サービスやベルトコンベアだと考えてください。

Streamは、以下のような複数回発生する可能性のあるイベントを扱うのに最適です。

  • ユーザーの入力イベント(例:テキストフィールドの変更)
  • サーバーからのリアルタイムデータ(例:WebSocket、Firebase)
  • ファイルI/Oのチャンク(分割データ)
  • タイマーのような定期的なイベント

実践的な例:リアルタイム時計

1秒ごとに現在時刻を生成するStreamを作成できます。Flutterでは、`StreamBuilder`ウィジェットがStreamをリッスンし、新しい値が届くたびにUIを再構築するのに最適なツールです。


import 'dart:async';

// 1秒ごとに現在時刻を生成して配信するStreamを返す関数
Stream<String> timedCounter() {
  return Stream.periodic(Duration(seconds: 1), (i) {
    return DateTime.now().toIso8601String();
  });
}

// Flutterウィジェット内での使用例:
// Widget build(BuildContext context) {
//   return StreamBuilder<String>(
//     stream: timedCounter(), // リッスンするStream
//     builder: (context, snapshot) {
//       if (snapshot.connectionState == ConnectionState.waiting) {
//         return Text("時計を初期化中...");
//       } else if (snapshot.hasError) {
//         return Text("エラー: ${snapshot.error}");
//       } else if (snapshot.hasData) {
//         // Streamが新しい値を生成するたびにTextウィジェットを再構築
//         return Text("現在時刻: ${snapshot.data}", style: TextStyle(fontSize: 24));
//       } else {
//         return Text("時刻データがありません。");
//       }
//     },
//   );
// }

結論:どれをいつ使うか

これら3つの概念をマスターすることで、応答性が高く高性能なFlutterアプリケーションを構築できます。タスクに適したツールを選ぶためのシンプルなガイドは以下の通りです。

  • `async`/`await`と`Future`:ネットワークリクエストやデータベースアクセスなど、結果を待つ必要がある一度きりの非同期処理(主にI/Oバウンドなタスク)に使用します。
  • `Isolate`(`compute`経由で):UIスレッドをフリーズさせる可能性のある、短時間で完了するCPU負荷の高い計算(画像処理や大規模なデータ構造の解析など)に使用します。
  • `Stream`:バックエンドからのリアルタイムデータ、継続的なユーザー入力、タイマーイベントなど、時間とともに発生する一連の非同期イベントを扱う場合に使用します。

これらのパターンを効果的に適用することは、アプリのパフォーマンスを向上させるだけでなく、提供するユーザー体験の質を格段に高めることにつながります。


0 개의 댓글:

Post a Comment