はじめに: 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データを扱いやすいモデルを作成する。
// 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に関して
part
と part of
指令により、user.freezed.dart
ファイルが user.dart
ファイルのプライベートメンバーにアクセスできるようになる。user.dart
ファイルで part 'user.freezed.dart';
を宣言することにより、user.freezed.dart
は user.dart
の一部として扱われ、user.dart
内のプライベートメンバー(プライベート関数、変数など)を使用できるようになる。
@JsonKey アノテーションに関して
@JsonKey
アノテーションは、JSONのキー名とDartのプロパティ名が異なる場合に、明示的なマッピングを定義するために使用される。これがなければ、シリアライザはJSONデータをDartオブジェクトに正確に変換できない。
例えば、APIからのJSONデータで "user_name"
というキーがある場合、@JsonKey(name: 'user_name')
アノテーションを使用して、このキーの値をDartオブジェクト内の username
プロパティに割り当てる。
{
"user_name": "テスト 太郎"
}
@JsonKey(name: 'user_name') required String username,
build_runnerコマンドで、Freezedとjson_serializableを使ったコードの自動生成
上記のコードをプロジェクトに追加した後、プロジェクトのルートディレクトリで、以下のコマンドを実行してFreezedクラスとJSONシリアライゼーションコードを自動生成する。
flutter pub run build_runner build
FVMパッケージを利用している場合は、fvmを先頭につける
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
に関連するファイルのみを生成する。
flutter pub run build_runner build --build-filter="lib/data/entities/drug_record.*"
もし、以前のビルドからの古い生成ファイルが残っていて、新しいファイル生成プロセスと競合し、エラーにより自動的にファイルが生成されないときは、--delete-conflicting-outputs
フラグを付けて build_runner
コマンドを実行し、コンフリクトしている出力ファイルを自動的に削除して、ファイル生成することができる。
flutter pub run build_runner build --delete-conflicting-outputs
APIパスの定義: APIエンドポイントの設定
api_path.dart
APIエンドポイントのURIを定義する。これにより、APIのパスを一元管理し、エンドポイントを容易に参照できるようにする。
// 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ライブラリを使ってAPIリクエストを行うクラスを定義している。ここでは、getUserInfo
メソッドがユーザー情報を取得するためのAPIリクエストを実行する。エラーハンドリングも含めて、APIのレスポンスを適切に処理する。
// 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
は個々のユーザー情報のデータ構造を表す。
// 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構造が以下のようになっている場合:
{
"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
クラスに変換して返す。
// 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に結びつける役割を担う。
// 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コンポーネントを格納する。
// 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アプリでダミーデータを使って開発を進める方法については下記に記載。
コメント