1. HOME
  2. ブログ
  3. エンジニアリング
  4. ジェネリクスを活用してFreezedモデルを効率よく定義する【Flutter】

BLOG

ブログ

エンジニアリング

ジェネリクスを活用して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モデル定義を簡潔に書けることがお分かりいただけたと思います。

  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

関連記事