完全自分用の学習備忘録。
詳細な解説等はコード内でコメントアウトしております。
モデルの作成とAPIサービス周りの処理を実装していきます。
モデルを作成
モデル作成後、以下のコマンドを実行し、〜.g.dart
ファイルを生成
flutter pub run build_runner build
-
lib/models/auth_model.dart
import 'package:json_annotation/json_annotation.dart'; part 'auth_model.g.dart'; @JsonSerializable(explicitToJson: true) class AuthModel { final String? username; final String email; final String password; final String? jwtToken; AuthModel({ this.username, required this.email, required this.password, this.jwtToken, }); factory AuthModel.fromJson(Map<String, dynamic> json) => _$AuthModelFromJson(json); static Map<String, dynamic> toJson(AuthModel instance) => _$AuthModelToJson(instance); }
-
lib/models/to_do_model.dart
import 'package:json_annotation/json_annotation.dart'; part 'to_do_model.g.dart'; @JsonSerializable(explicitToJson: true) class ToDoModel { final int id; String title; bool isCompleted; ToDoModel({ required this.id, required this.title, this.isCompleted = false, }); factory ToDoModel.fromJson(Map<String, dynamic> json) => _$ToDoModelFromJson(json); static Map<String, dynamic> toJson(ToDoModel instance) => _$ToDoModelToJson(instance); ToDoModel copyWith({ String? title, bool? isCompleted, }) { return ToDoModel( id: this.id, title: title ?? this.title, isCompleted: isCompleted ?? this.isCompleted, ); } }
-
lib/models/custom_error.dart
class CustomError extends Error { final int statsCode; final String message; CustomError(this.statsCode, this.message); @override String toString() => message; }
-
lib/models/custom_exception.dart
class CustomException implements Exception { final int statsCode; final String title = 'エラーが発生しました。'; final String message; CustomException( this.statsCode, this.message, ); }
APIサービス
-
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'; }
-
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'; } } }
-
lib/api/service/result.dart
作成後、以下のコマンドを実行
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>; }
-
lib/api/service/common_http_router.dart
APIリクエストの共通部分を定義する抽象クラスを作成する
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, ), ); } }
-
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); }