【Flutter:hooks_riverpod × Freezed】APIからのJSONレスポンスをもとにユーザー情報表示

この記事は約16分で読めます。

はじめに: Hooks_Riverpod、Freezedの概要

Hooks_Riverpod: flutter_hooks ライブラリと組み合わせて使用されるRiverpodのバージョン。FlutterのHook機能を活用し、より宣言的で簡潔なコードを書くことができる

Freezed: 不変の状態を持つデータクラスを生成することで、安全でメンテナンスしやすいコードを実現する。

ここでは、hooks_riverpodとFreezedを使用して、APIからのJSONレスポンスをもとにFlutterアプリでユーザー情報を表示する一連の流れをまとめる。

api_client.dart でAPIエンドポイント(api_path.dartで定義)からデータを取得し、そのJSONオブジェクトを UserResponse クラスを使用してDartオブジェクトに変換する。この変換は user_repository.dart で行われる。そして、変換されたユーザーデータをRiverpodプロバイダーを通じてUI層(user_view.dart)で表示する。

開発フローの例(ユーザー情報表示)

エンティティ/モデルの作成:

  • Freezedを使用して、ユーザー情報を格納するエンティティ(例: User)を定義。
  • build_runnerコマンドで、Freezedとjson_serializableを使ったコードを自動生成する

APIパスの定義:

  • api_path.dart にユーザー情報APIエンドポイント(例: /user/info)を追加。

APIクライアントの実装:

  • api_client.dart にユーザー情報取得メソッドを追加。
  • APIクライアントがAPIエンドポイントを利用してデータを取得

Responseクラスの実装:

  • APIレスポンスをパースするためのクラス(例: UserResponse)を responses ディレクトリに作成。

リポジトリの実装:

  • repositories にユーザー情報の取得と管理のビジネスロジックを実装。
  • リポジトリクラスがAPIクライアントを介してユーザー情報を取得し、そのデータを UserResponse オブジェクト(Dartオブジェクト)に変換する

Riverpodプロバイダーの設定:

  • ユーザー情報をUIに提供するためのRiverpodプロバイダーを設定。Riverpodプロバイダーがリポジトリのメソッドを使用して非同期にユーザー情報を取得し、UIに公開

UIの構築:

  • UIコンポーネントがRiverpodプロバイダーを使ってユーザー情報を表示

階層構造例

lib/
├── constants/
│   └── api_path.dart
├── models/
│   ├── user.dart
│   ├── user.freezed.dart
│   └── user.g.dart
├── services/
│   └── api_client.dart
├── repositories/
│   └── user_repository.dart
├── responses/
│   ├── user_response.dart
│   ├── user_response.freezed.dart
│   └── user_response.g.dart
├── providers/
│   └── user_provider.dart
└── views/
    └── user_view.dart

開発順に見ていく。

エンティティ/モデルの作成: Freezedを使用したデータモデルの定義

Freezedを使用して、不変性を持ちつつJSONデータを扱いやすいモデルを作成する。

Dart
// models/user.dart
import 'package:freezed_annotation/freezed_annotation.dart';

// 生成されるdartファイルを記述
part 'user.freezed.dart';
part 'user.g.dart';

// freezedでコード生成するために「@freezed」を記述
@freezed
class User with _$User { // withの後には「_$[class name]」の形式で記述

  // プロパティを指定
  const factory User({
    required int id,
    required String username,
    required String email
  }) = _User;

  // サーバーから送られてくるJSONデータをDartのオブジェクトに変換する
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

factory User.fromJson(Map<String, dynamic> json) メソッドは、サーバーから受け取ったJSONデータをFlutter(Dart)のオブジェクトに変換するために使われる。これは「JSONデシリアライゼーション」と呼ばれ、このメソッドはjson_serializableパッケージにより自動生成される。

JSONシリアライズ:

  • Dartオブジェクト(または他の言語のオブジェクト)をJSON形式の文字列に変換するプロセス。つまり、プログラム内のデータ構造をJSON形式のテキストに変換して、外部のシステムでの使用や、HTTPを介した通信、ファイルへの保存などに使用できるようにする。

JSONデシリアライズ:

  • 逆に、JSONデシリアライズはJSON形式の文字列をDartオブジェクトに変換するプロセス。これにより、JSON形式のデータをプログラム内で扱いやすいオブジェクトやデータ構造に変換して、アプリケーションのロジックで使用できるようになる。

partとpart ofに関して

partpart of 指令により、user.freezed.dart ファイルが user.dart ファイルのプライベートメンバーにアクセスできるようになる。user.dart ファイルで part 'user.freezed.dart'; を宣言することにより、user.freezed.dartuser.dart の一部として扱われ、user.dart 内のプライベートメンバー(プライベート関数、変数など)を使用できるようになる。

@JsonKey アノテーションに関して

@JsonKey アノテーションは、JSONのキー名とDartのプロパティ名が異なる場合に、明示的なマッピングを定義するために使用される。これがなければ、シリアライザはJSONデータをDartオブジェクトに正確に変換できない。

例えば、APIからのJSONデータで "user_name" というキーがある場合、@JsonKey(name: 'user_name') アノテーションを使用して、このキーの値をDartオブジェクト内の username プロパティに割り当てる。

Dart
{
  "user_name": "テスト 太郎"
}
Dart
@JsonKey(name: 'user_name') required String username,

build_runnerコマンドで、Freezedとjson_serializableを使ったコードの自動生成

上記のコードをプロジェクトに追加した後、プロジェクトのルートディレクトリで、以下のコマンドを実行してFreezedクラスとJSONシリアライゼーションコードを自動生成する。

Zsh
flutter pub run build_runner build

FVMパッケージを利用している場合は、fvmを先頭につける

Zsh
fvm flutter pub run build_runner build

上記、build_runner コマンドにより、.freezed.dart.g.dart の2種類のファイルが生成される。

  • .freezed.dart ファイル:Freezedによる不変性とコピー機能を持つクラス。
  • .g.dart ファイル:json_serializableによるJSONのデシリアライゼーションとシリアライゼーションメソッドが含まれている。これによりJSONデータの読み書きが容易になる。

_$UserFromJson メソッドは .g.dart ファイルに自動生成されるメソッド。このメソッドは json_serializable パッケージによって提供され、JSONデータをDartのオブジェクトに変換するためのロジックを含んでいる。この自動生成プロセスにより、開発者はJSONのパースやマッピングを手動で書く必要がなく、効率的にデータモデリングを行うことができる。

build_runner の実行時に特定のファイルのみを対象にすることもできる。例えば、以下のコマンドは drug_record.dart に関連するファイルのみを生成する。

Zsh
flutter pub run build_runner build --build-filter="lib/data/entities/drug_record.*"

もし、以前のビルドからの古い生成ファイルが残っていて、新しいファイル生成プロセスと競合し、エラーにより自動的にファイルが生成されないときは、--delete-conflicting-outputs フラグを付けて build_runner コマンドを実行し、コンフリクトしている出力ファイルを自動的に削除して、ファイル生成することができる。

Zsh
flutter pub run build_runner build --delete-conflicting-outputs

APIパスの定義: APIエンドポイントの設定

api_path.dart

APIエンドポイントのURIを定義する。これにより、APIのパスを一元管理し、エンドポイントを容易に参照できるようにする。

Dart
// constants/api_path.dart
class ApiPaths {
  static const String userInfo = '/user/info';
}

APIクライアントの実装: Dioを用いたAPI通信の実装

HTTPリクエストを実行するためのクライアントを提供する。Dio(FlutterとDart用の強力で使いやすいHTTPクライアントライブラリ)や他のHTTPクライアントライブラリを使用して、API呼び出しの実装を行う。ここにはAPIの各メソッド(GET, POST, PUT, DELETEなど)に対するロジックが含まれる。

dio | Dart package
A powerful HTTP networking package, supports Interceptors, Aborting and canceling a request, Custom adapters, Transforme...

Dioライブラリを使ってAPIリクエストを行うクラスを定義している。ここでは、getUserInfo メソッドがユーザー情報を取得するためのAPIリクエストを実行する。エラーハンドリングも含めて、APIのレスポンスを適切に処理する。

Dart
// services/api_client.dart
import 'package:dio/dio.dart';

class ApiClient {
  Dio _dio;

  ApiClient(this._dio);

  Future<dynamic> getUserInfo() async {
    try {
      final response = await _dio.get(ApiPaths.userInfo);
      return response.data;
    } catch (e) {
      throw Exception('Failed to load user info');
    }
  }
}

Responseクラスの実装

APIからのレスポンス全体を表すUserResponseクラスを定義。

このクラスはFreezedを使用して不変性を確保し、JSONから直接インスタンス化するためのfromJsonメソッドを含んでいる。

modelsディレクトリに定義されるUserクラスとの違いは、UserResponseがAPIレスポンスの全体構造(Userオブジェクトの集合)を表すのに対し、Userは個々のユーザー情報のデータ構造を表す。

Dart
// responses/user_response.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:your_project/models/user.dart';

part 'user_response.freezed.dart';
part 'user_response.g.dart';

@freezed
class UserResponse with _$UserResponse {
  const factory UserResponse({
    required User user,
  }) = _UserResponse;

  factory UserResponse.fromJson(Map<String, dynamic> json) => _$UserResponseFromJson(json);
}

Responseクラスは、APIレスポンスのルートレベルにあるJSONキーと値のペアを扱う。
例えば、APIレスポンスのJSON構造が以下のようになっている場合:

JSON
{
  "medical_bills": [
    {
      "id": 1,
      "medical_treatment_year_month": "2024-02",
      "total_amount": 7777,
      "details": [
        {
          "medical_institution_name": "メルプ内科クリニック",
          "amount_of_payment": 5432
        },
        {
          "medical_institution_name": "メルプ調剤薬局",
          "amount_of_payment": 2345
        }
      ]
    },
    // 他のレコード...
  ]
}

MedicalBillsRecordsResponse@JsonKey(name: 'medical_bills') アノテーションは、このJSONの "medical_bills" キーに対応している必要がある。そして、このキーの値(レコードの配列)は MedicalBillsRecord.fromJson メソッドを使って、Dartの MedicalBillsRecord オブジェクトのリストにデシリアライズされる。medical_billsの中の各プロパティに関しては、entities or modelのフォルダで処理する。

こちらも、build_runner コマンドを使用して必要な補助ファイルを生成する。

リポジトリの実装: データアクセス層の設計

リポジトリの実装では、userRepository クラスを作成して、ユーザー情報の取得に関連するビジネスロジックを実装する。このクラスはAPIクライアントを使用してユーザーデータを取得し、それを返すメソッドを提供する。

getUser メソッドがAPIクライアントを使用してユーザーデータを取得し、そのデータを UserResponse クラスに変換して返す。

Dart
// repositories/user_repository.dart
import 'package:your_project/services/api_client.dart';
import 'package:your_project/responses/user_response.dart';

class UserRepository {
  final ApiClient _apiClient;

  UserRepository(this._apiClient);

  Future<UserResponse> getUser() async {
    final responseData = await _apiClient.getUserInfo();
    return UserResponse.fromJson(responseData);
  }
}

Riverpodプロバイダーの設定: 状態管理とデータフロー

FutureProviderを使って非同期的にユーザー情報を取得するRiverpodプロバイダーを作成している。userProviderは、UI部分で利用され、APIからユーザーデータを取得し、結果を画面に反映させるために使用される。

通常、リポジトリはデータアクセス層(APIクライアントとの通信、データ変換など)に集中し、Riverpodプロバイダーは別の層(状態管理とデータフロー制御)に配置されるため、分離しておく。リポジトリは純粋なビジネスロジックを提供し、プロバイダーはこれらのロジックをUIに結びつける役割を担う。

Dart
// providers/user_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:your_project/repositories/user_repository.dart';

final userProvider = FutureProvider<User>((ref) async {
  return ref.read(userRepositoryProvider).getUser();
});

UIの構築: データを使用したウィジェットの作成

UIの構築では、Riverpodプロバイダーを使用してユーザー情報を画面に表示。以下のコード例では、RiverpodのHookConsumerWidgetを使ってユーザー情報を取得し、画面に表示している。

今回はviewsディレクトリ配下に記載しているが、presentationディレクトリを作成して、その中にpages, styles, widgetsフォルダを作成して、pagesの中に記載するのもあり。

pages フォルダは画面ごとのUIコンポーネントを、styles はスタイリング関連のコードを、widgets は再利用可能なUIコンポーネントを格納する。

Dart
// views/user_view.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:your_project/providers/user_provider.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class UserScreen extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsyncValue = ref.watch(userProvider);

    return Scaffold(
      body: userAsyncValue.when(
        data: (user) => Text('Welcome, ${user.username}'),
        loading: () => CircularProgressIndicator(),
        error: (e, _) => Text('Error: $e'),
      ),
    );
  }
}

終了!

サーバー側のAPIが実装されていない間に、Flutterアプリでダミーデータを使って開発を進める方法については下記に記載。

コメント

タイトルとURLをコピーしました