Flutter初心者の学習記録 第10回:Try-Catchとカスタム例外の作成

こんにちは、ジミーです!今回は、Flutterアプリケーション開発において非常に重要な「例外処理」について詳しく解説します。特にTry-Catchブロックの使い方とカスタム例外の作成方法に焦点を当てていきましょう。

例外処理の重要性

アプリ開発において、エラーは避けられないものです。ネットワーク接続の問題、APIからの予期せぬレスポンス、ユーザー入力の不正など、様々な要因でアプリがクラッシュする可能性があります。

適切な例外処理を実装することで:

  • アプリのクラッシュを防止できる
  • ユーザーに分かりやすいエラーメッセージを提供できる
  • デバッグが容易になる
  • ユーザー体験を向上させることができる

Dartの基本的なTry-Catch構文

Dartでは、以下のような構文で例外処理を行います:

try {
  // 例外が発生する可能性のあるコード
} catch (e) {
  // 例外が発生した場合の処理
} finally {
  // 例外の有無にかかわらず実行される処理
}

例えば、JSONのパースを行う場合:

try {
  final data = jsonDecode(jsonString);
  print('パース成功: $data');
} catch (e) {
  print('JSONパースエラー: $e');
} finally {
  print('処理完了');
}

特定の例外タイプをキャッチする

Dartでは、特定のタイプの例外だけをキャッチすることも可能です:

try {
  // 例外が発生する可能性のあるコード
} on FormatException catch (e) {
  print('フォーマットエラー: $e');
} on SocketException catch (e) {
  print('ネットワークエラー: $e');
} catch (e) {
  print('その他のエラー: $e');
}

カスタム例外の作成

アプリケーション固有のエラーを表現するために、カスタム例外クラスを作成すると便利です。Dartでは、Exceptionインターフェースを実装するか、Errorクラスを継承することでカスタム例外を作成できます。

基本的なカスタム例外

class NetworkException implements Exception {
  final String message;
  final int statusCode;

  NetworkException(this.message, this.statusCode);

  @override
  String toString() {
    return 'NetworkException: $message (Status Code: $statusCode)';
  }
}

複数のカスタム例外クラス

アプリケーションの異なる部分で発生するエラーを区別するために、複数のカスタム例外クラスを作成すると良いでしょう:

// APIリクエスト関連の例外
class ApiException implements Exception {
  final String message;
  final int statusCode;

  ApiException(this.message, this.statusCode);

  @override
  String toString() => 'ApiException: $message (Status Code: $statusCode)';
}

// 認証関連の例外
class AuthException implements Exception {
  final String message;

  AuthException(this.message);

  @override
  String toString() => 'AuthException: $message';
}

// データベース関連の例外
class DatabaseException implements Exception {
  final String message;
  final String operation;

  DatabaseException(this.message, this.operation);

  @override
  String toString() => 'DatabaseException: $message (Operation: $operation)';
}

実践的な使用例

それでは、これらのカスタム例外を使用した実践的な例を見てみましょう。

APIサービスでの使用例

class ApiService {
  final http.Client _client = http.Client();
  
  Future<Map<String, dynamic>> fetchData(String endpoint) async {
    try {
      final response = await _client.get(Uri.parse(endpoint));
      
      if (response.statusCode == 200) {
        return jsonDecode(response.body);
      } else if (response.statusCode == 401 || response.statusCode == 403) {
        throw AuthException('認証エラーが発生しました');
      } else {
        throw ApiException('APIリクエストに失敗しました', response.statusCode);
      }
    } on SocketException {
      throw NetworkException('ネットワーク接続エラー', 0);
    } on FormatException {
      throw ApiException('レスポンスの形式が不正です', 0);
    } catch (e) {
      throw Exception('予期せぬエラーが発生しました: $e');
    }
  }
}

UIでの例外処理

class _MyHomePageState extends State<MyHomePage> {
  late Future<Map<String, dynamic>> _dataFuture;
  final ApiService _apiService = ApiService();

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  void _loadData() {
    _dataFuture = _apiService.fetchData('https://api.example.com/data');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('例外処理デモ'),
      ),
      body: Center(
        child: FutureBuilder<Map<String, dynamic>>(
          future: _dataFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return CircularProgressIndicator();
            } else if (snapshot.hasError) {
              // エラータイプに応じて異なるUIを表示
              if (snapshot.error is NetworkException) {
                return _buildErrorWidget('ネットワークエラー', '通信環境をご確認ください');
              } else if (snapshot.error is AuthException) {
                return _buildErrorWidget('認証エラー', '再度ログインしてください');
              } else if (snapshot.error is ApiException) {
                final error = snapshot.error as ApiException;
                return _buildErrorWidget('APIエラー', 'エラーコード: ${error.statusCode}');
              } else {
                return _buildErrorWidget('エラー', snapshot.error.toString());
              }
            } else if (snapshot.hasData) {
              // データの表示
              return ListView.builder(
                itemCount: snapshot.data!.length,
                itemBuilder: (context, index) {
                  final key = snapshot.data!.keys.elementAt(index);
                  return ListTile(
                    title: Text(key),
                    subtitle: Text(snapshot.data![key].toString()),
                  );
                },
              );
            } else {
              return Text('データがありません');
            }
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _loadData();
          });
        },
        child: Icon(Icons.refresh),
      ),
    );
  }

  Widget _buildErrorWidget(String title, String message) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.error, color: Colors.red, size: 60),
        SizedBox(height: 16),
        Text(
          title,
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
        SizedBox(height: 8),
        Text(message),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _loadData();
            });
          },
          child: Text('再試行'),
        ),
      ],
    );
  }
}

エラーハンドリングのベストプラクティス

  1. エラー発生場所の近くで処理する:エラーが発生した場所に近いレイヤーで例外をキャッチすると、コンテキストを失わずに処理できます。
  2. 適切な粒度のカスタム例外を作成する:アプリケーションのドメインに合わせた例外を作成しましょう。
  3. エラーメッセージは具体的に:開発者やユーザーが理解しやすいエラーメッセージを提供しましょう。
  4. リソースのクリーンアップを忘れないfinallyブロックやtry-catch-finally構造を活用して、例外発生時もリソースを確実に解放しましょう。
  5. 例外のログ記録:デバッグのために例外情報をログに記録しましょう。
try {
  // 何らかの処理
} catch (e, stackTrace) {
  // eは例外オブジェクト、stackTraceはスタックトレース
  log('エラーが発生しました: $e\n$stackTrace');
  rethrow; // 必要に応じて例外を再スロー
}

非同期コードでの例外処理

Flutterの非同期処理(Future, async/await)でも、try-catchを使用できます:

Future<void> fetchAndProcessData() async {
  try {
    final data = await apiService.fetchData();
    final processedData = processData(data);
    await saveToDatabase(processedData);
  } on NetworkException catch (e) {
    showErrorDialog('ネットワークエラー', e.message);
  } on ApiException catch (e) {
    showErrorDialog('APIエラー', '${e.message} (${e.statusCode})');
  } catch (e) {
    showErrorDialog('エラー', e.toString());
  }
}

まとめ

適切な例外処理とカスタム例外の作成は、堅牢なFlutterアプリケーションを構築するための重要な要素です。例外をただキャッチするだけでなく、アプリケーションのドメインに合わせたカスタム例外を設計し、ユーザーに分かりやすいフィードバックを提供することが大切です。

コメント欄で皆さんのエラーハンドリングのテクニックやアイデアをぜひ共有してください。皆さんと一緒にFlutterの学習を続けられることを楽しみにしています!


この記事が役に立ちましたか?他に知りたいトピックがあれば、コメントでお知らせください!

コメントを残す

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