Flutter初心者の学習記録 第9回:FutureとAsync/Awaitの完全ガイド

こんにちは、ジミーです!今回のブログでは、Flutter開発において非常に重要な概念である「Future」と「async/await」について詳しく解説していきます。非同期プログラミングはモバイルアプリ開発において必須のスキルですので、是非最後までお付き合いください。

Futureとは何か?

FlutterにおけるFutureは、「将来的に完了する処理の結果」を表現するオブジェクトです。APIリクエスト、ファイル読み込み、データベース操作など、時間のかかる処理を行う際に使用します。

Futureは次の2つの状態のいずれかになります:

  1. 未完了(incomplete) - 処理が進行中
  2. 完了(complete) - 処理が終了し、値を持つか、エラーが発生

基本的なFutureの使い方を見てみましょう:

Future<String> fetchUserData() {
  return Future.delayed(
    Duration(seconds: 2),
    () => "ユーザーデータ取得完了",
  );
}

void main() {
  print("データ取得開始");
  
  fetchUserData().then((data) {
    print(data);
  }).catchError((error) {
    print("エラー発生: $error");
  }).whenComplete(() {
    print("処理完了");
  });
  
  print("他の処理を実行中...");
}

このコードを実行すると、次のような出力になります:

データ取得開始
他の処理を実行中...
ユーザーデータ取得完了
処理完了

注目すべき点は、fetchUserData()の処理が完了するのを待たずに、「他の処理を実行中...」というメッセージが表示されていることです。これが非同期処理の特徴です。

async/awaitによる読みやすいコード

Futureは強力ですが、.then().catchError()を多用すると、いわゆる「コールバック地獄」に陥る可能性があります。そこで登場するのがasync/await構文です。

async/awaitを使うと、非同期コードを同期的に書いているかのように読みやすく記述できます:

Future<void> loadUserData() async {
  try {
    print("データ取得開始");
    
    // awaitを使用すると、この行で処理が一時停止し、
    // Futureが完了するまで次の行に進みません
    String data = await fetchUserData();
    
    print(data);
    print("データ処理完了");
  } catch (e) {
    print("エラー発生: $e");
  }
}

void main() async {
  await loadUserData();
  print("メイン処理終了");
}

この出力は次のようになります:

データ取得開始
ユーザーデータ取得完了
データ処理完了
メイン処理終了

ポイントは以下の通りです:

  1. asyncキーワードは、その関数が非同期処理を含むことを示します
  2. awaitキーワードは、Futureの結果が得られるまで処理を一時停止します
  3. try-catchブロックを使って例外処理が可能です

実践的な例:HTTPリクエスト

実際のアプリ開発では、APIからデータを取得することが多いでしょう。httpパッケージを使った例を見てみましょう:

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

Future<List<User>> fetchUsers() async {
  final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
  
  if (response.statusCode == 200) {
    List<dynamic> jsonList = jsonDecode(response.body);
    return jsonList.map((json) => User.fromJson(json)).toList();
  } else {
    throw Exception('Failed to load users');
  }
}

class User {
  final int id;
  final String name;
  final String email;
  
  User({required this.id, required this.name, required this.email});
  
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}

これをUIで使用するには、FutureBuilderウィジェットが便利です:

class UserListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ユーザー一覧')),
      body: FutureBuilder<List<User>>(
        future: fetchUsers(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('エラー: ${snapshot.error}'));
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('データがありません'));
          } else {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                User user = snapshot.data![index];
                return ListTile(
                  title: Text(user.name),
                  subtitle: Text(user.email),
                );
              },
            );
          }
        },
      ),
    );
  }
}

複数のFutureを同時に処理する

複数の非同期処理を同時に実行したい場合は、Future.waitが役立ちます:

Future<void> loadAllData() async {
  try {
    final results = await Future.wait([
      fetchUsers(),
      fetchPosts(),
      fetchComments(),
    ]);
    
    final users = results[0] as List<User>;
    final posts = results[1] as List<Post>;
    final comments = results[2] as List<Comment>;
    
    print('取得したユーザー数: ${users.length}');
    print('取得した投稿数: ${posts.length}');
    print('取得したコメント数: ${comments.length}');
  } catch (e) {
    print('データ取得中にエラー発生: $e');
  }
}

注意点とベストプラクティス

  1. UI応答性を保つ: 重い処理は必ず非同期で行い、メインスレッドをブロックしないようにしましょう
  2. エラーハンドリングを忘れずに: 非同期処理ではエラーが発生しやすいため、try-catchで適切に対応しましょう
  3. 状態管理との連携: ProviderやBloc、Riverpodなどの状態管理ライブラリと組み合わせて、非同期データの状態を適切に管理しましょう
  4. キャンセレーション: 必要に応じて処理をキャンセルできるようにすることも検討しましょう
// キャンセル可能な非同期処理の例
Future<void> cancelableOperation() async {
  final completer = Completer<void>();
  
  // キャンセルフラグ
  bool isCancelled = false;
  
  // キャンセル関数
  void cancel() {
    isCancelled = true;
    if (!completer.isCompleted) {
      completer.completeError(Exception('操作がキャンセルされました'));
    }
  }
  
  // バックグラウンド処理
  Future<void> doWork() async {
    for (int i = 0; i < 10; i++) {
      if (isCancelled) return;
      await Future.delayed(Duration(seconds: 1));
      print('処理中... $i');
    }
    
    if (!completer.isCompleted) {
      completer.complete();
    }
  }
  
  // 処理開始
  doWork();
  
  // 5秒後にキャンセル
  Future.delayed(Duration(seconds: 5), cancel);
  
  return completer.future;
}

まとめ

Flutterでの非同期プログラミングは、Futureasync/awaitの理解から始まります。これらのツールを使いこなすことで、ネットワーク通信やファイル操作などの時間のかかる処理をスムーズに実装でき、ユーザー体験の良いアプリを開発することができます。

一見難しく感じるかもしれませんが、実践を通じて徐々に理解を深めていきましょう。

ご質問やご意見があれば、コメント欄でお待ちしています!


本記事がお役に立てば幸いです。次回もお楽しみに!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です