完全自分用の学習備忘録。
詳細な解説等はコード内でコメントアウトしております。
以下の記事を参考に実装していきます。
Flutter でスマートに非同期処理のエラーハンドリングをする方法 - Qiita
使用するライブラリ・パッケージ
記事の通り、Result型
を使用します。
-
pubspec.yaml
dependencies: flutter: sdk: flutter dio: ^5.7.0 freezed: freezed_annotation:
APIリクエスト処理の設計と実装
各APIエンドポイントのURLを定義します。
(エンドポイントはプロジェクトに合わせて自由に書き換えてください)
-
lib/api/service/api_url.dart
class ApiUrl { /// 今回はローカルサーバーのみ static const String baseUrl = 'http://localhost:8080'; /// 認証 static const String signUp = '/auth/sign_up'; static const String signIn = '/auth/login'; /// ToDo static const String fetchToDos = '/todos'; static const String addToDo = '/todos'; static const String editToDo = '/todos'; static const String deleteToDo = '/todos'; }
HTTP Method をenumで管理します。
-
lib/api/service/http_method.dart
enum HttpMethod { GET, POST, PUT, DELETE; String get name { switch (this) { case HttpMethod.GET: return 'GET'; case HttpMethod.POST: return 'POST'; case HttpMethod.PUT: return 'PUT'; case HttpMethod.DELETE: return 'DELETE'; } } }
freezed を使って Result 型を作ります。
-
lib/api/service/result.dart
作成後、以下のコマンドを実行します。
エラー(Error)が発生した場合は、throwをしてプログラムを終了させるため、Result型にはException(例外)のみをResultでラップするようにしています。
dart run build_runner watch --delete-conflicting-outputs
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:spotify_app/api/service/api_service.dart'; import 'package:spotify_app/model/custom_exception.dart'; part 'result.freezed.dart'; @freezed abstract class Result<T> with _$Result<T> { const factory Result.success(T value) = Success<T>; const factory Result.exception(CustomException exception) = ResultException<T>; }
APIリクエストの共通部分を定義する抽象クラスを作成します。
-
lib/api/service/common_http_router.dart
import 'package:dio/dio.dart'; import 'package:spotify_app/api/service/api_url.dart'; import 'package:spotify_app/config/config.dart'; import 'package:spotify_app/config/storage.dart'; import 'http_method.dart'; // APIリクエストの共通部分を定義する抽象クラス // 各APIリクエストの詳細なパスやパラメータ、ボディなどはサブクラスで実装 abstract class CommonHttpRouter { // 基本URL String baseUrl = ApiUrl.baseUrl; // 各APIエンドポイントのパス String get path; // GET、POST、PUT、DELETE HttpMethod get method; // ヘッダー Future<Map<String, String>> get headers async => { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer ${ await SecureStorage().load( Config.secureStorageJwtTokenKey)}', // 端末のローカルに保存したJWT認証トークンをヘッダーに追加 }; // パスパラメーター List<String>? get pathParameters => null; // クエリパラメーター Map<String, dynamic>? get queryParameters => null; // ボディ Object? body() => null; // パスパラメーターとクエリパラメーターを組み合わせる String get combinedPathAndQueryParameters { String combinedPath = path; if (pathParameters != null) { combinedPath += '/${pathParameters!.join('/')}'; } if (queryParameters != null) { combinedPath += '?${queryParameters!.entries .map((entry) => '${entry.key}=${entry.value}') .join('&')}'; } return combinedPath; } // HTTP通信をするためのDioインスタンスを生成 Future<Dio> get dio async { final headers = await this.headers; return Dio( BaseOptions( baseUrl: baseUrl, headers: headers, method: method.name, responseType: ResponseType.json, connectTimeout: Config.apiDuration, ), ); } }
APIリクエスト処理を定義していきます。
abstract class UseCase<I, O>
を作成します。
-
lib/api/service/api_service.dart
import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:spotify_app/api/service/result.dart'; import 'package:spotify_app/model/custom_error.dart'; import 'package:spotify_app/model/custom_exception.dart'; import 'common_http_router.dart'; import 'http_method.dart'; /* abstract class UseCase<I, O> I → Input リクエストするデータの型を指定する CommonHttpRouter O → Output レスポンスの型を指定する Map<String, dynamic>? */ class ApiService extends UseCase<CommonHttpRouter, Map<String, dynamic>?> { // UseCaseクラス内のFuture<O> execute(I request);の「O, I」に型を指定 // excute()をoverrideをして具体的なAPIリクエスト処理を定義し、 // リクエスト成功時 → Map<String, dynamic>?, 失敗時 → CustomExceptionまたはCustomErrorをthrowするようにする // ViewModel側でUseCaseをcall()し、Result型を返す @override Future<Map<String, dynamic>?> execute(CommonHttpRouter request) async { final dio = await request.dio; Response? response; try { if (request.method == HttpMethod.GET) { response = await dio.request( request.combinedPathAndQueryParameters ); } else { response = await dio.request( request.combinedPathAndQueryParameters, data: request.body(), ); } print(response.data); print(response.statusCode); print(response.statusMessage); if (response == null) { throw CustomError(999, 'レスポンスがありません。'); } if (response.statusCode! >= 200 && response.statusCode! < 300) { return response.data['data']; } else { throw CustomError(response.statusCode ?? 999, response.statusMessage ?? '不明なエラーが発生しました。'); } } on DioException catch (exception) { throw CustomException(exception.response?.statusCode ?? 999, exception.message ?? '不明なエラーが発生しました。'); } on Exception catch (exception) { throw CustomException(999, exception.toString()); } } } /* abstract class UseCase<I, O> I → Input リクエストするデータの型を指定する CommonHttpRouter O → Output レスポンスの型を指定する Map<String, dynamic>? */ abstract class UseCase<I, O> { Future<Result<O>> call(I request) async { try { final data = await execute(request); // ここでAPIリクエストを実行 return Result.success(data); } on CustomException catch (exception) { return Result.exception(exception); } on CustomError catch (error) { // エラーの場合はプログラムを終了させるためError用のResult型は定義していない debugPrint('statusCode: ${error.statsCode.toString()}'); debugPrint('message: ${error.message}'); throw error; } } Future<O> execute(I request); }
使用例
実際にどのように使用するのか実装していきます。
まず、リクエストする情報を組み立てていきます。例として、
- サインアップリクエスト
- ToDoタスクの新規作成リクエスト
- ToDoタスクの更新リクエスト
を組み立てていきます。
認証(サインアップ / サインイン)
-
lib/api/requests/sign_up_request.dart
import 'package:spotify_app/api/service/api_url.dart'; import 'package:spotify_app/api/service/common_http_router.dart'; import 'package:spotify_app/model/auth_model.dart'; import '../../../service/http_method.dart'; class SignUpRequest extends CommonHttpRouter { final AuthModel model; SignUpRequest(this.model); @override String get path => ApiUrl.signUp; @override HttpMethod get method => HttpMethod.POST; @override Object? body() => AuthModel.toJson(model); }
-
lib/api/requests/sign_in_request.dart
import 'package:spotify_app/api/service/api_url.dart'; import 'package:spotify_app/api/service/common_http_router.dart'; import 'package:spotify_app/model/auth_model.dart'; import '../../../service/http_method.dart'; class SignInRequest extends CommonHttpRouter { final AuthModel model; SignInRequest(this.model); @override String get path => ApiUrl.signIn; @override HttpMethod get method => HttpMethod.POST; @override Object? body() => AuthModel.toJson(model); }
Todo(新規作成 / 更新 )
-
lib/api/requests/add_to_do_request.dart
import 'dart:convert'; import 'package:spotify_app/api/service/api_url.dart'; import 'package:spotify_app/api/service/common_http_router.dart'; import '../../../service/http_method.dart'; class AddToDoRequest extends CommonHttpRouter { final String title ; AddToDoRequest(this.title); @override String get path => ApiUrl.addToDo; @override HttpMethod get method => HttpMethod.POST; @override Object? body() => {'title': title}; }
-
lib/api/requests/update_to_do_request.dart
import 'package:spotify_app/api/service/api_url.dart'; import 'package:spotify_app/api/service/common_http_router.dart'; import 'package:spotify_app/model/to_do_model.dart'; import '../../../service/http_method.dart'; class UpdateToDoRequest extends CommonHttpRouter { final ToDoModel model; UpdateToDoRequest(this.model); @override String get path => ApiUrl.editToDo; @override HttpMethod get method => HttpMethod.PUT; @override List<String>? get pathParameters => [model.id.toString()]; @override Object? body() => ToDoModel.toJson(model); }
APIリクエストの呼び出しもとを実装していきます。
-
main.dart
import 'package:flutter/material.dart'; import 'package:spotify_app/api/requests/post/auth/sign_in_request.dart'; import 'package:spotify_app/api/requests/post/auth/sign_up_request.dart'; import 'package:spotify_app/api/requests/post/todo/add_to_do_request.dart'; import 'package:spotify_app/api/requests/post/todo/update_to_do_request.dart'; import 'package:spotify_app/api/service/api_service.dart'; import 'package:spotify_app/api/service/common_http_router.dart'; import 'package:spotify_app/model/custom_exception.dart'; import 'package:spotify_app/model/to_do_model.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // APIサービスのインスタンス final ApiService apiService = ApiService(); // 認証ロジック Future<void> handleAuthRequest(CommonHttpRouter request) async { try { final result = await apiService(request); result.when( success: (Map<String, dynamic>? json) async { final authModel = AuthModel.fromJson(json!); await SecureStorage().save(Config.secureStorageJwtTokenKey, authModel.jwtToken ?? ''); print("Authentication successful: ${authModel.jwtToken}"); }, exception: (CustomException error) { print("Authentication failed: ${error.message}"); }, ); } catch (e) { print("Unexpected error: $e"); } } Future<void> signUp(String username, String email, String password) async { final authModel = AuthModel(username: username, email: email, password: password); final signUpRequest = SignUpRequest(authModel); await handleAuthRequest(signUpRequest); } Future<void> signIn(String email, String password) async { final authModel = AuthModel(email: email, password: password); final signInRequest = SignInRequest(authModel); await handleAuthRequest(signInRequest); } // ToDoロジック List<ToDoModel> todos = []; Future<void> fetchToDos() async { try { final fetchToDosRequest = FetchToDosRequest(); final result = await apiService(fetchToDosRequest); result.when( success: (Map<String, dynamic>? json) { todos = (json!['todos'] as List<dynamic>) .map((todoJson) => ToDoModel.fromJson(todoJson)) .toList(); print("Fetched ToDos: $todos"); }, exception: (CustomException error) { print("Failed to fetch ToDos: ${error.message}"); }, ); } catch (e) { print("Unexpected error: $e"); } } Future<void> addTodo(String title) async { try { final addToDoRequest = AddToDoRequest(title); final result = await apiService(addToDoRequest); result.when( success: (Map<String, dynamic>? json) { final newToDo = ToDoModel.fromJson(json!); todos.add(newToDo); print("Added ToDo: $newToDo"); }, exception: (CustomException error) { print("Failed to add ToDo: ${error.message}"); }, ); } catch (e) { print("Unexpected error: $e"); } } Future<void> removeTodo(int id) async { try { final deleteToDoRequest = DeleteToDoRequest(id); final result = await apiService(deleteToDoRequest); result.when( success: (_) { todos.removeWhere((todo) => todo.id == id); print("Removed ToDo with ID: $id"); }, exception: (CustomException error) { print("Failed to remove ToDo: ${error.message}"); }, ); } catch (e) { print("Unexpected error: $e"); } } Future<void> updateTodo(int id, String newTitle, bool isCompleted) async { try { final toDoModel = ToDoModel(id: id, title: newTitle, isCompleted: isCompleted); final updateToDoRequest = UpdateToDoRequest(toDoModel); final result = await apiService(updateToDoRequest); result.when( success: (_) { todos = todos.map((todo) { return todo.id == id ? todo.copyWith(title: newTitle, isCompleted: isCompleted) : todo; }).toList(); print("Updated ToDo with ID: $id"); }, exception: (CustomException error) { print("Failed to update ToDo: ${error.message}"); }, ); } catch (e) { print("Unexpected error: $e"); } } // サンプル実行 await signUp("testuser", "test@example.com", "password123"); await signIn("test@example.com", "password123"); await fetchToDos(); await addTodo("New Task"); await updateTodo(1, "Updated Task", true); await removeTodo(1); }