Freezed入門:Flutterで型安全なモデル設計

IT開発

Flutterアプリ開発において、データクラスの作成で冗長なコードを書いていませんか?equals、hashCode、toString、copyWithメソッドを手動で実装するのは面倒で、バグの温床にもなりがちです。そんな問題を解決してくれるのがFreezedです。

今回は、実際のクイズアプリプロジェクトを例に、Freezedを使った型安全なモデル設計の実践方法を紹介します。

参考プロジェクト:https://github.com/morisakisan/share_quiz

Freezedとは何か?

Freezedは、Dartでimmutable(不変)なデータクラスを簡単に作成できるコード生成ライブラリです。アノテーションを使って宣言するだけで、equals、hashCode、toString、copyWithなどのメソッドを自動生成してくれます。また、JSON serialization/deserializationにも対応しており、APIとの連携も簡単に行えます。

使い所

Freezedは主にデータモデルクラスの作成に使用します。特にFirestoreなどのデータベースとの連携、API通信でのデータ転送、状態管理でのデータ保持など、アプリケーション全体でデータを安全に扱いたい場面で威力を発揮します。immutableなオブジェクトを簡単に作成できるため、予期しないデータ変更を防ぎ、バグの少ない堅牢なアプリケーションを構築できます。

ライブラリの読み込み

pubspec.yamlに以下を追加します:

dependencies:
  freezed_annotation: 3.1.0

dev_dependencies:
  freezed: 3.2.3
  build_runner: 2.8.0
  json_serializable: 6.11.1  # JSON対応が必要な場合

実際のコード例

ここからは、実際のクイズアプリプロジェクトで使用しているFreezedのコードを見ていきましょう。Clean Architectureを採用したプロジェクトで、domain層とdata層でFreezedを活用している例です。

ドメインモデルでの基本的な使用

アプリのコアとなるQuizモデルの例です。

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'quiz.freezed.dart';

@freezed
abstract class Quiz with _$Quiz {
  const factory Quiz({
    required String documentId,
    required String title,
    required String question,
    required List<String> choices,
    required int correctAnswer,
    required DateTime? createdAt,
    required double? correctAnswerRate,
    required int? answerCount,
    required int? goodCount,
    required List<String> imageUrls,
  }) = _Quiz;
}

ポイント:

  • @freezedアノテーションで自動生成を指定
  • const factoryでimmutableなコンストラクタを定義
  • requiredで必須パラメータを明示
  • nullable型(?)で任意パラメータを表現

JSON対応のDTOクラス

FirestoreとのデータやりとりでJSON serialization/deserializationが必要なDTOクラスの例です。

import 'package:flutter/foundation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:share_quiz/data/json_converter/timestamp_converter.dart';

part 'quiz_dto.freezed.dart';
part 'quiz_dto.g.dart';

@freezed
abstract class QuizDto with _$QuizDto {
  const factory QuizDto({
    @JsonKey(includeFromJson: true, includeToJson: true) String? docId,
    @JsonKey(name: 'correct_answer') required int correctAnswer,
    @JsonKey(name: 'title') required String title,
    @JsonKey(name: 'question') required String question,
    @JsonKey(name: 'image_url') required List<String> imageUrl,
    @JsonKey(name: 'choices') required List<String> choices,
    @TimestampConverter()
    @JsonKey(name: 'created_at') required DateTime? createdAt,
    @JsonKey(name: 'uid') required String uid,
    @JsonKey(name: 'correct_answer_rate') required double? correctAnswerRate,
    @JsonKey(name: 'answer_count') required int? answerCount,
    @JsonKey(name: 'good_count') required int? goodCount,
  }) = _QuizDto;

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

ポイント:

  • part 'quiz_dto.g.dart';でJSON serialization用ファイルを指定
  • @JsonKey(name: 'field_name')でJSONフィールド名をマッピング
  • @TimestampConverter()でカスタムコンバーターを使用
  • factory fromJsonでJSON deserializationメソッドを定義

カスタムコンバーターの実装

FirestoreのTimestamp型をDateTime型に変換するカスタムコンバーターの例です。

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

class TimestampConverter implements JsonConverter<DateTime?, Object> {
  const TimestampConverter();

  @override
  DateTime? fromJson(Object timestamp) {
    if (timestamp is Timestamp) {
      return timestamp.toDate();
    }
    return null;
  }

  @override
  Object toJson(DateTime? date) {
    if (date == null) {
      return FieldValue.serverTimestamp();
    }
    return Timestamp.fromDate(date);
  }
}

ポイント:

  • JsonConverterインターフェースを実装
  • fromJsonでJSONからDartオブジェクトに変換
  • toJsonでDartオブジェクトからJSONに変換
  • Firestore特有のTimestamp型を適切に処理

フォーム用のモデル

ユーザー入力を扱うフォーム用のモデルの例です。

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'quiz_form.freezed.dart';

@freezed
abstract class QuizForm with _$QuizForm {
  const factory QuizForm({
    String? title,
    String? question,
    File? image,
    List<String>? choices,
    int? answer,
  }) = _QuizForm;
}

ポイント:

  • すべてのフィールドをnullableにしてフォームの段階的入力に対応
  • File型で画像ファイルを扱う
  • シンプルな構造でフォームデータを管理

コード生成の実行

Freezedのコードを書いた後は、以下のコマンドでコード生成を実行します:

# 一回だけ実行
flutter packages pub run build_runner build

# ファイル変更を監視して自動実行
flutter packages pub run build_runner watch

Freezedの利点

型安全性:
コンパイル時に型チェックが行われ、実行時エラーを防げます。

Immutability:
オブジェクトが不変なため、予期しないデータ変更を防げます。

コード削減:
equals、hashCode、toString、copyWithメソッドが自動生成されます。

JSON対応:
json_serializableと組み合わせて簡単にJSON変換が可能です。

パターンマッチング:
Union typesを使った高度なパターンマッチングも可能です。

まとめ

Freezedを使うことで、型安全で保守しやすいデータモデルを簡単に作成できます。特にFirebaseやAPIとの連携が多いFlutterアプリケーションでは、JSON serialization/deserializationの機能と組み合わせることで、開発効率が大幅に向上します。

今回紹介した基本的な使い方をマスターすることで、バグの少ない堅牢なFlutterアプリケーションを構築できるようになります。Clean Architectureと組み合わせることで、さらに保守性の高いコードベースを実現できます。

コメント

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