🔄

FlutterアプリにおけるRiverpod非同期処理のエラーハンドリングとリトライ戦略の実装

に公開

本記事はChatGPTにより生成されました。


1. 導入:テーマの概要や重要性

Flutterアプリの開発において、非同期処理はAPI通信やデータベースアクセス、位置情報取得など多くの場面で不可欠です。非同期処理はユーザー体験を向上させる一方で、ネットワーク障害やサーバーエラーなどの例外状況に対応するためのエラーハンドリングが重要となります。さらに、適切なリトライ戦略を組み込むことで、ユーザーにとってストレスの少ない堅牢なアプリケーションを実現できます。

Flutterの状態管理ライブラリ「Riverpod」は、非同期処理の管理をシンプルかつ強力に行える点で注目されています。特に、AsyncValueを用いたエラーステートの扱いや、リトライ処理の導入は、個人開発者やスタートアップのエンジニアにとって、堅実なアプリ設計を行う上で必須の技術です。

本記事では、Riverpodの非同期プロバイダーで発生するエラーを適切にハンドリングし、リトライ戦略を実装する方法を、実践的なコード例と共に解説します。特にマップや位置情報を活用したサービスなど、ネットワーク依存が高いモバイルアプリ開発に役立つ内容を目指します。


2. 背景・基礎知識

Riverpodとは

RiverpodはFlutterの状態管理ライブラリで、Providerの進化版として設計されました。Providerよりもスコープ管理やテスト容易性に優れ、非同期処理の管理も強力です。FutureProviderStreamProviderなど、非同期データを扱うための専用プロバイダーが備わっています。

AsyncValue

非同期処理の結果はRiverpodのAsyncValue<T>型で表現されます。これは以下の3状態を持ちます。

  • AsyncLoading:読み込み中
  • AsyncData:正常に取得できたデータ
  • AsyncError:エラー発生時

この3状態を適切にUIへ反映し、エラー時の表示やリトライ制御を行うことが重要です。

エラーハンドリングの必要性

ネットワーク通信は常に成功するとは限らず、タイムアウトやサーバーエラー、認証エラーなど多様な例外が発生します。適切にエラーを捕捉し、ユーザーに分かりやすく通知することはUX向上に必須です。

リトライ戦略の重要性

一度の通信失敗で諦めるのではなく、指数的バックオフや最大試行回数の設定などリトライ戦略を組み込むことで、ネットワークの一時的な不良を乗り越えやすくなります。特にモバイル環境では通信状況が不安定なことが多いため効果的です。


3. 本論:技術的な詳細や仕組み、手順

非同期プロバイダーでのエラーハンドリング

Riverpodでは、FutureProviderなどの非同期プロバイダーで返されたAsyncValuewhenmaybeWhenメソッドを使い、状態ごとにUIや処理を分けるのが基本です。

final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  if (response.statusCode != 200) throw Exception('Failed to load user');
  return User.fromJson(jsonDecode(response.body));
});

// UI側
Widget build(BuildContext context, WidgetRef ref) {
  final userAsync = ref.watch(userProvider);
  return userAsync.when(
    data: (user) => Text('Hello, ${user.name}'),
    loading: () => CircularProgressIndicator(),
    error: (error, stack) => ErrorWidgetWithRetry(error: error),
  );
}

リトライ戦略の実装

リトライを実装する際は、以下のポイントを考慮します。

  • 最大リトライ回数の設定
  • リトライ間隔(固定 or 指数的バックオフ)
  • リトライ時の状態管理
  • UIからのリトライトリガー

RiverpodのStateNotifierAsyncNotifierを活用し、リトライロジックをビジネスロジック層に組み込むのが推奨されます。

アーキテクチャ例

+-----------------+        +-------------------+        +-----------------------+
|    UI Widget    | <----> | Riverpod Provider | <----> | 非同期処理(API呼び出し) |
+-----------------+        +-------------------+        +-----------------------+
         ↑                            ↑                           ↑
         |                            |                           |
  エラーステート表示           AsyncValue管理              リトライロジック内蔵

4. 具体例・コード例

以下は、位置情報APIを呼び出し、非同期処理のエラーをハンドルしつつ、ユーザー操作や自動リトライを可能にしたサンプル実装です。

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// 位置情報取得の例外クラス
class LocationException implements Exception {
  final String message;
  LocationException(this.message);
}

/// 位置情報のモデル
class Location {
  final double latitude;
  final double longitude;
  Location(this.latitude, this.longitude);
}

/// 非同期で位置情報を取得する関数(擬似API)
Future<Location> fetchLocation() async {
  // 擬似的に失敗・成功を切り替え
  final isSuccess = DateTime.now().second % 3 != 0; // 3秒周期で失敗
  await Future.delayed(Duration(seconds: 1));
  if (!isSuccess) throw LocationException('位置情報の取得に失敗しました');
  return Location(35.681236, 139.767125);
}

/// リトライ可能なAsyncNotifier
class LocationNotifier extends AsyncNotifier<Location> {
  static const int maxRetry = 3;
  int _retryCount = 0;

  
  Future<Location> build() async {
    return await _fetchWithRetry();
  }

  Future<Location> _fetchWithRetry() async {
    while (true) {
      try {
        final location = await fetchLocation();
        _retryCount = 0; // 成功したらリトライ回数リセット
        return location;
      } catch (e) {
        if (_retryCount >= maxRetry) {
          rethrow;
        }
        _retryCount++;
        await Future.delayed(Duration(seconds: 2 * _retryCount)); // 指数的バックオフ
      }
    }
  }

  Future<void> retry() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _fetchWithRetry());
  }
}

final locationProvider =
    AsyncNotifierProvider<LocationNotifier, Location>(() => LocationNotifier());

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(home: LocationScreen());
  }
}

class LocationScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final locationAsync = ref.watch(locationProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod非同期処理のエラーハンドリング')),
      body: Center(
        child: locationAsync.when(
          data: (location) => Text(
              '現在地: 緯度 ${location.latitude}, 経度 ${location.longitude}'),
          loading: () => CircularProgressIndicator(),
          error: (error, _) => Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('エラー: ${error is LocationException ? error.message : error.toString()}'),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: () => ref.read(locationProvider.notifier).retry(),
                child: Text('再試行'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

手順説明

  1. LocationNotifierAsyncNotifierを継承し、非同期処理を担当。
  2. _fetchWithRetryメソッドで最大3回までのリトライを

Discussion