Dartは、特にFlutterのバックエンドとして人気が高まっているプログラミング言語です。
非同期プログラミングは、このDartにおいて重要な要素となっています。
なぜなら、ユーザーインターフェースのスムーズな操作性を維持するために不可欠だからです。
本記事では、Dartの非同期プログラミングの主要な機能について解説します。
具体的には、Future
、Stream
、そしてIsolatesの使い方とベストプラクティスを紹介します。
非同期プログラミングの重要性
非同期プログラミングには、重要な利点があります。
それは、UIスレッドをブロックせずに、バックグラウンドでタスクを実行できることです。
そのため、アプリケーションの応答性を維持したまま、時間のかかる処理を実行できます。
例えば、ネットワーク通信やファイルI/Oなどの処理が該当します。
async/awaitの基本
Dartでは、async
とawait
というキーワードを使います。
これにより、非同期コードを同期的に書くことができます。
その結果、コードの可読性が向上します。
また、エラーハンドリングも容易になります。
import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('Fetch Example')), body: const FetchDemo(), ), ); } } class FetchDemo extends StatefulWidget { const FetchDemo({super.key}); @override State<FetchDemo> createState() => _FetchDemoState(); } class _FetchDemoState extends State<FetchDemo> { String result = 'ボタンを押してデータを取得'; Future<void> fetchData() async { try { final response = await http.get( Uri.parse('https://jsonplaceholder.typicode.com/todos/1') ); setState(() { result = response.body; }); } catch (e) { setState(() { result = 'Error: $e'; }); } } @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(result), ElevatedButton( onPressed: fetchData, child: const Text('データを取得'), ), ], ), ); } }
この例では、fetchData
関数を非同期で実行します。
その結果はresponse
に格納されます。
もしエラーが発生した場合は、catch
ブロックで処理します。
FutureとStreamの使い分け
Future
Future
は、単一の非同期操作の結果を表現します。
主に、一度だけ結果が返ってくる操作に使用します。
例えば、HTTPリクエストの応答やファイルの読み込みなどが該当します。
Future<String> fetchUserData(String userId) async { final response = await http.get( Uri.parse('https://api.example.com/users/$userId'), ); if (response.statusCode == 200) { return response.body; } else { throw HttpException('Failed to fetch user data'); } }
Stream
Stream
は、複数の非同期イベントを処理するために使います。
継続的なデータフローを扱う場合に適しています。
例えば、WebSocketからのリアルタイムデータ受信や、ファイルの逐次読み込みなどで使用します。
Stream<String> websocketStream() async* { // WebSocketサーバーに接続 final socket = await WebSocket.connect('wss://example.com'); try { // 受信したメッセージをストリームとして処理 await for (var message in socket) { yield message as String; } } finally { // 終了時に必ずソケットを閉じる await socket.close(); } } void handleWebSocketMessages() async { try { // ストリームからメッセージを受信して処理 await for (var message in websocketStream()) { print('Received: $message'); } } catch (e) { print('WebSocket error: $e'); } }
とりあえずは、以下の関連を覚えておきましょう。
単発: Future + async + await 連続: Stream + async* + yield + (await for)
Isolateを使用した並行処理
Dartには、Isolateという機能があります。
これは、重い計算処理をメインisolateから分離するために使用します。
各isolateは独立したメモリ空間で実行されます。
そして、メッセージパッシングを通じて通信を行います。
import 'dart:isolate'; void main() async { // 0から999999までの数字のリストを生成 final numbers = List.generate(1000000, (i) => i); try { // 重い計算を実行して結果を待つ final result = await performHeavyComputation(numbers); print('Computation completed'); // 結果の一部を表示(確認用) print('First 5 results: ${result.take(5)}'); print('Last 5 results: ${result.skip(result.length - 5)}'); } catch (e) { print('Computation failed: $e'); } } Future<List<int>> performHeavyComputation(List<int> numbers) async { // Isolate.runを使って別のisolateで実行 return await Isolate.run(() { return numbers.map((n) => n * n).toList(); }); }
以下は、Dart/FlutterのWeb版(dart4web)でdart:isolateパッケージを使用しようとした際に発生するものです。
Computation failed: Unsupported operation: dart:isolate is not supported on dart4web
Webプラットフォームではサポートされていません。
Webブラウザの JavaScript が基本的にシングルスレッドモデルで動作するからです。
エラーハンドリングのベストプラクティス
非同期処理では、適切なエラーハンドリングが重要です。
以下に、包括的なエラーハンドリングの例を示します。
Future<void> robustAsyncOperation() async { try { await Future.wait([ operation1(), operation2(), operation3(), ]); } on NetworkException catch (e) { // ネットワークエラーの特別な処理 print('Network error: $e'); } on TimeoutException catch (e) { // タイムアウトの特別な処理 print('Operation timed out: $e'); } catch (e) { // その他のエラー print('Unexpected error: $e'); } finally { // リソースのクリーンアップ await cleanup(); } }
非同期プログラミングのベストプラクティス
非同期プログラミングを効果的に行うために、重要なベストプラクティスを紹介します。
適切なタイムアウトの設定
ネットワーク通信などの非同期処理には、適切なタイムアウトを設定することが重要です。
Future<String> fetchWithTimeout() async { return await fetchData().timeout( Duration(seconds: 5), onTimeout: () => throw TimeoutException('Operation timed out'), ); }
キャンセル可能な操作の実装
ユーザーの操作や画面遷移によって処理をキャンセルできるようにすることで、リソースの無駄遣いを防ぎます。
class DataService { CancelToken? _cancelToken; Future<void> fetchData() async { _cancelToken = CancelToken(); try { final response = await dio.get( 'https://api.example.com/data', cancelToken: _cancelToken, ); // データ処理 } catch (e) { if (e is DioError && e.type == DioErrorType.cancel) { print('Operation was cancelled'); } rethrow; } } void cancelOperation() { _cancelToken?.cancel('Operation cancelled by user'); } }
エラーの適切な処理と伝播
非同期処理では、エラーを適切にキャッチし、必要に応じて上位層に伝播することが重要です。
Future<void> loadData() async { try { final data = await fetchData(); await processData(data); } on NetworkException catch (e) { // ネットワークエラーの特別な処理 throw UserFriendlyException('通信エラーが発生しました'); } catch (e) { // 予期しないエラーの処理 throw UserFriendlyException('予期しないエラーが発生しました'); } finally { // リソースのクリーンアップ await cleanup(); } }
リソースの適切な解放
非同期処理で使用したリソースは、適切なタイミングで解放することが重要です。
class DataManager { final _subscription = dataStream.listen(/* ... */); void dispose() { _subscription.cancel(); } }
実装時の注意点
- 複数の非同期処理を並行して行う場合はFuture.waitを使用する
- UI更新を伴う処理は必ずsetStateや状態管理の仕組みを通して行う
- デバッグ時はエラーの発生箇所を特定しやすいよう、適切なエラーメッセージを設定する
- 重い処理は適切にIsolateを使用する(ただしWeb版での制限に注意)
まとめ
Dartの非同期プログラミングには、3つの主要な概念があります。
それは、Future
、Stream
、そしてIsolatesです。
これらを適切に使い分けることが重要です。
そうすることで、効率的で応答性の高いアプリケーションを開発できます。
各機能の使用目的は以下の通りです。
Future
は単一の非同期操作に使用Stream
は継続的なデータフローに使用- Isolateは重い計算処理の分離に使用
また、適切なエラーハンドリングも重要です。
そして、ベストプラクティスを適用することも必要です。
これらを組み合わせることで、堅牢な非同期処理を実装できます。