0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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

モデルの作成と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);
    }
    
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?