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

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

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

認証画面用とToDoリスト画面用のViewModel、それぞれのAPIエンドポイントにリクエストする情報の組み立て処理を実装していきます。

### ViewModelを作成

以下のViewModelを作成する。

- AuthViewModel
- ToDoViewModel
- lib_providers/auth_view_model.dart
    
    ```dart
    import 'package:riverpod_annotation/riverpod_annotation.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/service/api_service.dart';
    import 'package:spotify_app/api/service/common_http_router.dart';
    import 'package:spotify_app/model/auth_model.dart';
    import 'package:spotify_app/model/custom_exception.dart';
    import 'package:spotify_app/config/config.dart';
    import 'package:spotify_app/config/storage.dart';
    
    part 'auth_view_model.g.dart';
    
    // ユーザー認証のロジックを管理するViewModel
    @riverpod
    class AuthViewModel extends _$AuthViewModel {
      // APIリクエストを実行するためのサービスクラス
      final ApiService _apiService = ApiService();
    
      @override
      Future<bool> build() async {
        // 初期状態では認証が完了していないため、falseを返す
        return false;
      }
    
      // 共通の認証リクエスト処理
      //`CommonHttpRouter`型のリクエストを受け取り、APIを呼び出す
      // 成功時: バックエンド（Golang）から受け取ったJWT認証トークンを端末のローカル上に保存し、stateに認証完了(true)を設定して、画面遷移
      // 失敗時: stateにエラー（カスタム例外クラス）を設定して、エラー状態にする（エラーアラートを表示）
      Future<void> _handleAuthRequest(CommonHttpRouter request) async {
        // 状態をローディング中に設定
        state = const AsyncValue.loading();
        // APIリクエスト実行
        final result = await _apiService(request);
    
        // Result型でリクエスト結果をハンドリング
        result.when(
            success: (Map<String, dynamic>? json) async {
              // JSONデータをAuthModelに変換
              final authModel = AuthModel.fromJson(json!);
              // JWTトークンをセキュアストレージに保存
              await SecureStorage().save(Config.secureStorageJwtTokenKey, authModel.jwtToken ?? '');
              // 認証成功として状態を更新
              state = const AsyncValue.data(true);
            },
            exception: (CustomException error) {
              // エラーの場合、状態をエラーとして更新
              state = AsyncError(error, StackTrace.empty);
            }
        );
      }
    
      // サインアップ
      Future<void> signUp(String username, String email, String password) async {
        final authModel = AuthModel(username: username, email: email, password: password);
        // リクエストする情報を組み立てる
        final signUpRequest = SignUpRequest(authModel);
        _handleAuthRequest(signUpRequest);
      }
    
      // サインイン
      Future<void> signIn(String email, String password) async {
        final authModel = AuthModel(email: email, password: password);
        // リクエストする情報を組み立てる
        final signInRequest = SignInRequest(authModel);
        _handleAuthRequest(signInRequest);
      }
    }
    ```
    
- lib_providers/to_do_view_model.dart
    
    ```dart
    import 'package:riverpod_annotation/riverpod_annotation.dart';
    import 'package:spotify_app/api/requests/post/todo/add_to_do_request.dart';
    import 'package:spotify_app/api/requests/post/todo/delete_to_do_request.dart';
    import 'package:spotify_app/api/requests/post/todo/fetch_to_dos_request.dart';
    import 'package:spotify_app/api/requests/post/todo/update_to_do_request.dart';
    import 'package:spotify_app/model/custom_exception.dart';
    import 'package:spotify_app/model/to_do_model.dart';
    import '../api/service/api_service.dart';
    
    part 'to_do_view_model.g.dart';
    
    @riverpod
    class ToDoViewModel extends _$ToDoViewModel {
      final ApiService _apiService = ApiService();
    
      @override
      Future<List<ToDoModel>> build() async {
        await fetchToDos(); // 初回取得
        return state.value ?? [];
      }
    
      // ToDoを取得
      Future<void> fetchToDos() async {
        state = const AsyncValue.loading();
        final fetchToDosRequest = FetchToDosRequest();
        final result = await _apiService(fetchToDosRequest);
    
        result.when(
            success: (Map<String, dynamic>? json) async {
              final List<dynamic> todosJson = json!['todos'];
              final List<ToDoModel> todos = todosJson
                  .map((todoJson) => ToDoModel.fromJson(todoJson as Map<String, dynamic>))
                  .toList();
              state = AsyncValue.data(todos);
            },
            exception: (CustomException error) {
              state = AsyncError(error, StackTrace.empty);
            }
        );
      }
    
      // ToDoを新規作成
      Future<void> addTodo(String title) async {
        state = const AsyncValue.loading();
        final addToDoRequest = AddToDoRequest(title);
        final result = await _apiService(addToDoRequest);
    
        result.when(
            success: (Map<String, dynamic>? json) async {
              final newToDo = ToDoModel.fromJson(json!);
              final currentList = state.value ?? [];
              final updatedList = [...currentList, newToDo];
              state = AsyncValue.data(updatedList);
            },
            exception: (CustomException error) {
              state = AsyncError(error, StackTrace.empty);
            }
        );
      }
    
      // ToDoを削除
      Future<void> removeTodo(int id) async {
        state = const AsyncValue.loading();
        final deleteToDoRequest = DeleteToDoRequest(id);
        final result = await _apiService(deleteToDoRequest);
    
        result.when(
            success: (Map<String, dynamic>? _) async {
              final updatedList = state.value?.where((todo) => todo.id != id).toList() ?? [];
              state = AsyncValue.data(updatedList);
            },
            exception: (CustomException error) {
              state = AsyncError(error, StackTrace.empty);
            }
        );
      }
    
      // ToDoを更新
      Future<void> updateTodo(int id, String newTitle, bool isCompleted) async {
        state = const AsyncValue.loading();
        final toDoModel = ToDoModel(id: id, title: newTitle, isCompleted: isCompleted);
        final updateToDoRequest = UpdateToDoRequest(toDoModel);
        final result = await _apiService(updateToDoRequest);
    
        result.when(
            success: (Map<String, dynamic>? _) async {
              final currentList = state.value ?? [];
              final updatedList = currentList.map((todo) {
                return todo.id == id ? todo.copyWith(title: newTitle, isCompleted: isCompleted) : todo;
              }).toList();
              state = AsyncValue.data(updatedList);
            },
            exception: (CustomException error) {
              state = AsyncError(error, StackTrace.empty);
            }
        );
      }
    }
    ```
    

### リクエストする情報を組み立てる

認証（サインアップ / サインイン）

- lib/api/requests/sign_up_request.dart
    
    ```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
    
    ```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
    
    ```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/create_to_do_request.dart
    
    ```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);
    }
    ```
    
- lib/api/requests/fetch_to_dos_request.dart
    
    ```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 FetchToDosRequest extends CommonHttpRouter {
    
      @override
      String get path => ApiUrl.fetchToDos;
    
      @override
      HttpMethod get method => HttpMethod.GET;
    }
    ```
    
- lib/api/requests/delete_to_do_reqeust.dart
    
    ```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 DeleteToDoRequest extends CommonHttpRouter {
      final int toDoId;
    
      DeleteToDoRequest(this.toDoId);
    
      @override
      String get path => ApiUrl.deleteToDo;
    
      @override
      HttpMethod get method => HttpMethod.DELETE;
    
      @override
      List<String>? get pathParameters => [toDoId.toString()];
    }
    ```