ジェネリクスを活用してFreezedモデルを効率よく定義する【Flutter】
Genericsが必要になるケース
Freezedを用いたモデル定義にジェネリクスを活用すると、よりシンプルなコード定義が可能になります。
共通のデータ構造が現れる時には、ジェネリクスを用いて共通化できるか検討すると良いでしょう。
本記事では、ページネーションされたAPIレスポンスをマッピングするケースを考えていきます。
{
"total": 78,
"per_page": 10,
"current_page": 3,
"last_page": 8,
"from": 31,
"to": 40,
"data":[
{
// Record...
},
{
// Record...
}
]
}
全体のソースコードはこちら
Freezedの使い方はこちらにまとめたので、必要でしたらご覧ください。
Freezedのインストール
flutter pub add freezed_annotation
flutter pub add dev:build_runner
flutter pub add dev:freezed
flutter pub add json_annotation
flutter pub add dev:json_serializable
Genericsを活用したモデル定義
lib/models/pagination/pagination.dart
// ignore_for_file: non_constant_identifier_names
import 'package:freezed_annotation/freezed_annotation.dart';
part 'pagination.freezed.dart';
part 'pagination.g.dart';
@Freezed(genericArgumentFactories: true)
sealed class Pagination<T> with _$Pagination<T> {
const factory Pagination({
required int total,
required int per_page,
required int current_page,
required int last_page,
required int from,
required int to,
required List<T> data,
}) = _Pagination;
factory Pagination.fromJson(
Map<String, dynamic> json, T Function(Object?) fromJsonT) =>
_$PaginationFromJson<T>(json, fromJsonT);
}
今回は、以下のような投稿(Post)データのJSONがページングされることを想定します。
{
"title": "Freezedを活用して美しいモデルを定義する【Flutter】",
"thumbnail": "https://techinit.co.jp/wp-content/uploads/2024/06/flutter-freezed-1.png",
"body": "ここにテキスト ここにテキスト...",
"published_at": "2024/01/01 19:24",
"updated_at": "2024/07/01 20:48"
}
投稿用のモデルも定義します。
lib/models/post/post.dart
// ignore_for_file: non_constant_identifier_names
import 'package:freezed_annotation/freezed_annotation.dart';
part 'post.freezed.dart';
part 'post.g.dart';
@freezed
class Post with _$Post {
const factory Post({
required String title,
required String thumbnail,
required String body,
required String published_at,
required String updated_at,
}) = _Post;
factory Post.fromJson(Map<String, Object?> json) => _$PostFromJson(json);
}
定義したら以下のコマンドを実行して、コードを自動生成します。
dart run build_runner build
モックAPIの作成
投稿一覧をページングして返すモックAPIを作成します。
lib/server.dart
import 'dart:convert';
final post = {
'title': 'ジェネリクスを活用してFreezedモデルを効率よく定義する【Flutter】',
'thumbnail':
'https://techinit.co.jp/wp-content/uploads/2024/06/flutter-freezed-generics.png',
'body': List.generate(10, (_) => 'ここにテキスト ').join(),
'published_at': '2024/01/01 19:24',
'updated_at': '2024/07/01 20:48',
};
Future<String> postsRes() async {
await Future.delayed(const Duration(seconds: 2));
final posts = List.generate(10, (_) => post);
return jsonEncode({
'total': 78,
'per_page': 10,
'current_page': 3,
'last_page': 8,
'from': 31,
'to': 40,
'data': posts,
});
}
データのシリアライズ
lib/api.dart
import 'dart:convert';
import 'package:flutter_freezed_generics/models/pagination/pagination.dart';
import 'package:flutter_freezed_generics/models/post/post.dart';
import 'package:flutter_freezed_generics/server.dart';
Future<Pagination<Post>> getPosts() async {
final res = await postsRes();
final json = jsonDecode(res);
return Pagination<Post>.fromJson(
json, (Object? json) => Post.fromJson(json as Map<String, dynamic>));
}
シリアライズロジックはこれだけです。あとは、画面に表示しましょう。
import 'package:flutter/material.dart';
import 'package:flutter_freezed_generics/api.dart';
import 'package:flutter_freezed_generics/models/pagination/pagination.dart';
import 'package:flutter_freezed_generics/models/post/post.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: _MyHomePage(),
);
}
}
class _MyHomePage extends StatefulWidget {
const _MyHomePage();
@override
State<_MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<_MyHomePage> {
late Future<Pagination<Post>> futurePosts;
@override
void initState() {
super.initState();
futurePosts = getPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('記事一覧'),
),
body: FutureBuilder(
future: futurePosts,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('${snapshot.error}');
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
return _PostListView(posts: snapshot.data!.data);
},
),
);
}
}
class _PostListView extends StatelessWidget {
final List<Post> posts;
const _PostListView({required this.posts});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
child: ListView(
children: posts.map((post) {
return ListTile(
leading: Image.network(post.thumbnail),
title: Text(post.title),
subtitle: Text(post.published_at));
}).toList()));
}
}
画面表示
まとめ
以上となります。Genericsを活用してFreezedモデル定義を簡潔に書けることがお分かりいただけたと思います。
この記事へのコメントはありません。