😺

【Flutter】Freezedに独自メソッドを追加する方法

に公開

はじめに

Flutter開発において、イミュータブルなデータクラスを生成するために必須級のパッケージである Freezed

通常はプロパティを定義して終わりですが、「firstNamelastName を結合した fullName を取得したい」や、「特定の条件を満たしているか判定するメソッドを持たせたい」と思ったことはありませんか?

しかし、単にメソッドを書くだけではエラーになってしまいます。
今回は、Freezedのモデルに独自のメソッドやGetterを追加するための「たった1行の重要な記述」について解説します。

参考
https://pub.dev/packages/freezed#adding-getters-and-methods-to-our-models

結論:プライベートコンストラクタを追加するだけ

Freezedで生成したクラスにメソッドを追加するには、空のプライベートコンストラクタ const ClassName._(); を定義する必要があります。

これがないと、Freezedの生成コードと競合してしまい、正しく動作しません。

コード例

以下は、User クラスに「成人かどうか」を判定する isAdult というGetterを追加する例です。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';


abstract class User with _$User {
  // ① ここが重要!空のプライベートコンストラクタ
  const User._();

  // 通常のファクトリコンストラクタ
  const factory User({
    required String name,
    required int age,
  }) = _User;

  // ② 追加したい独自メソッドやGetter
  bool get isAdult => age >= 20;
  
  void sayHello() {
    print('Hello, my name is $name');
  }
}

なぜこれが必要なのか?

Freezedは通常、データ保持のための入れ物として機能しますが、メソッドを追加したい場合は「このクラスはただのデータ定義ではなく、振る舞いを持つクラス(Base class)である」とDartに認識させる必要があります。

const User._(); という記述は、このクラスをインスタンス化可能なベースとして確立させるための「おまじない」のようなものだと覚えておくと良いでしょう。

公式ドキュメントより
"To enable method definitions, you simply need to define a private empty constructor."
(メソッド定義を有効にするには、プライベートな空のコンストラクタを定義するだけです。)

Riverpodとの相性が抜群に良くなる

このテクニックを知っておくと、Riverpodによる状態管理がよりスッキリ書けるようになります。

通常、ロジックは Notifier (ViewModel的な場所) に書きがちですが、データそのものに関わるロジックは Model(Freezedクラス) に持たせたほうが、コードの見通しが良くなるケースがあります(ドメインロジックの凝集)。

例:状態更新メソッドをModelに持たせる

例えば、カウンターアプリで数値を増やすロジックを考えてみます。

Before:Notifierにロジックが散らばる

// Notifier側で、copyWithを使って計算している
state = state.copyWith(count: state.count + 1);

After:Modelにロジックを集約

まず、Freezedモデル側に更新用メソッドを作ります。


abstract class CounterState with _$CounterState {
  const CounterState._(); // おまじない

  const factory CounterState({
    (0) int count,
  }) = _CounterState;

  // 自身のコピーを返すメソッドを定義
  CounterState increment() {
    return copyWith(count: count + 1);
  }
}

すると、RiverpodのNotifier側はこうなります。


class Counter extends _$Counter {
  
  CounterState build() => const CounterState();

  void increment() {
    // 非常に直感的!
    state = state.increment();
  }
}

このように、「どうデータを変更するか」というロジックをModel側に閉じ込めることができ、Riverpod側は「どのメソッドを呼ぶか」だけに集中できます。これを徹底すると、テストも書きやすくなり、変更に強いコードになります。

まとめ

  • Freezedモデルにメソッドを追加したいときは、const ClassName._(); を追加する。
  • これだけでGetterや複雑なロジックをモデル内に記述できる。
  • Riverpodと組み合わせる際、状態更新ロジックをModelに持たせることで、コードの責務が明確になる。

たった1行の追加ですが、開発の幅がぐっと広がるテクニックです。

Discussion