Go(JWT認証)✖️RiverpodでTodoアプリを作ってみたPart4（Flutterロジック実装① 編）

完全自分用の学習備忘録。

詳細な解説等はコード内でコメントアウトしております。

モデルの作成とAPIサービス周りの処理を実装していきます。

### モデルを作成

モデル作成後、以下のコマンドを実行し、`〜.g.dart`ファイルを生成

`flutter pub run build_runner build`

- lib/models/auth_model.dart
    
    ```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
    
    ```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
    
    ```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
    
    ```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
    
    ```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
    
    ```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`
    
    ```dart
    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リクエストの共通部分を定義する抽象クラスを作成する
    
    ```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,
          ),
        );
      }
    }
    ```
    
- lib/api/service/api_service.dart
    
    ```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);
    }
    ```