【Flutter / Dart】Equatableってなんぞや

勉強中にAIが生成してくれたモデルが `Equatable` を継承していました。

> 🤔 **ん？Equatableって何だっけ？**

自分なりに調べてまとめてみました！

## 🧠 Equatableとは？

**オブジェクト同士で比較できるようにするパッケージ！**

Dartにはもともと `==` や `hashCode` を使ってオブジェクトを比較する方法がありますが、
**Equatableを使うとその処理を超シンプルに書けます！**



## 📦 Equatableの導入方法

[equatable](https://pub.dev/packages/equatable)


まずはパッケージの追加方法から👇

### `pubspec.yaml`に以下を追記：

```yaml
dependencies:
  equatable: ^2.0.5
```




## ✅ 結論

>  **「Dart の `==` / `hashCode` を、最小限のコードで “値比較” に切り替えられる便利パッケージ」**


## コードベースで理解する Equatable

### Equatableを使わない場合

```dart
class User {
  final String name;
  final int age;

  User(this.name, this.age);
}

void main() {
  final user1 = User('太郎', 25);
  final user2 = User('太郎', 25);

  print(user1 == user2); // false 😢
}
```

 **結果はfalse！**
→ 異なるインスタンスとみなされてしまうためです。



### 手動で比較を実装すると…

```dart
@override
bool operator ==(Object other) {
  return other is User &&
    other.name == name &&
    other.age == age;
}

@override
int get hashCode => name.hashCode ^ age.hashCode;
```

書くのも面倒だし、プロパティが増えたら保守も大変…！


### ✅ Equatableを使った場合

```dart
import 'package:equatable/equatable.dart';

class User extends Equatable {
  final String name;
  final int age;

  User(this.name, this.age);

  @override
  List<Object?> get props => [name, age];
}
```

`props` リストに比較対象を追加するだけ！

```dart
void main() {
  final user1 = User('太郎', 25);
  final user2 = User('太郎', 25);

  print(user1 == user2); // true 🎉
}
```


## 🧩 `props`リストのポイントと注意点

### `props`とは？

>  **Equatableで比較対象とするプロパティを指定するgetterメソッド！**

比較したいものだけ `props` に追加しましょう💡
比較対象外にしたいものはリストから外すだけ！



### ⚠️ 注意：可変リストは直接比較されない！

```dart
class BadExample extends Equatable {
  final List<String> items;

  @override
  List<Object?> get props => [items]; // 同じ内容でも別リストなら≠
}
```

 たとえ同じ要素でも、**異なるリストインスタンス**なら比較結果は `false` に！



### ✅ 解決策：`List.from()`やスプレッド演算子を使う

```dart
SearchState(results: [...apiResults]) // これで比較可能に！
```

→ 新しいリストを作ることで、前回のリストと 内容 で比較できるようになります 



## 🧪 比較対象から外した場合の例

```dart
class User extends Equatable {
  final String name;
  final int age;
  final String id;

  User(this.name, this.age, this.id);

  @override
  List<Object?> get props => [name, age]; // idは比較しない！
}

void main() {
  final user1 = User('太郎', 25, 'abc123');
  final user2 = User('太郎', 25, 'xyz789');

  print(user1 == user2); // true 🎯
}
```

## ✨ `toString()`も自動生成できる！

Equatableには `stringify` という便利プロパティがあります！

```dart
class User extends Equatable {
  final String name;
  final int age;

  User(this.name, this.age);

  @override
  List<Object?> get props => [name, age];

  @override
  bool get stringify => true; // これでtoString()も自動生成！
}

print(User('太郎', 25)); // 出力: User(太郎, 25)
```

 ログ出力も見やすくなるので、デバッグにも最適！


## プロパティ追加時の違い

### 手動の場合（めんどい…）

```dart
@override
bool operator ==(Object other) {
  return other is User &&
    other.name == name &&
    other.age == age &&
    other.email == email &&  // 追加
    other.phone == phone;    // 追加
}

@override
int get hashCode =>
  name.hashCode ^
  age.hashCode ^
  email.hashCode ^   // 追加
  phone.hashCode;    // 追加
```

### ✅ Equatableなら（超ラク！）

```dart
@override
List<Object?> get props => [name, age, email, phone]; // 追加するだけ！
```

## 🔥 状態管理との相性も◎

検索画面で考えると分かりやすい！

### 商品クラスと検索状態

```dart
class Product extends Equatable {
  final String id;
  final String name;

  const Product(this.id, this.name);

  @override
  List<Object?> get props => [id, name];
}
```

```dart
class SearchState extends Equatable {
  final String keyword;
  final List<Product> results;
  final bool isLoading;

  const SearchState({
    required this.keyword,
    required this.results,
    required this.isLoading,
  });

  @override
  List<Object?> get props => [keyword, results, isLoading];
}
```

💡 ポイント
searchAPIから返されるresultsは、たとえ検索結果の内容が同じでも、APIを呼び出すたびに新しいリストインスタンスが生成されます。
しかしEquatableがリストの中身を一つひとつ比較してくれるおかげで、SearchState全体としては「同じ状態」だと正しく判定できるのです。


### 🔍 検索処理と結果の比較

```dart
class SearchNotifier extends StateNotifier<SearchState> {
  SearchNotifier() : super(
    SearchState(keyword: '', results: [], isLoading: false)
  );

  Future<void> search(String keyword) async {
    state = SearchState(
      keyword: keyword,
      results: state.results,
      isLoading: true,
    );

    final results = await searchAPI(keyword);

    state = SearchState(
      keyword: keyword,
      results: results,
      isLoading: false,
    );
  }
}
```



### 🧪 実際の挙動

```dart
// 通常検索
searchNotifier.search('本');     // 画面更新 ✅
searchNotifier.search('本棚');   // 画面更新 ✅

// 同じ結果の再検索
await searchNotifier.search('本棚'); 
// → 新しいStateインスタンスは作られるが、
//   Riverpodは古いStateと新しいStateを `==` で比較する。
// → Equatableが「前と同じ内容」と判定するため `true` となり、
//   Riverpodは「状態は変化していない」と判断して再描画をスキップ！ 🎉
```

> Equatableにより**同じ状態と判断される＝無駄な再描画を防げる！**


### Equatableを使わないと…

```dart
// 🚨 Equatableなしの具体的な問題
// 1. スクロール位置がリセット 
// 2. 選択状態が消える 
// 3. アニメーションが途切れる 
// 4. バッテリー消費増加 
print('SearchScreen全体が再ビルドされました！');
```

毎回ビルドされてパフォーマンス悪化…💦

## 📝 まとめ

### Equatableのメリット

| メリット              | 内容                        |
| ----------------- | ------------------------- |
| ✅ コードが簡潔          | `==` / `hashCode`の記述が不要！  |
| ✅ 比較漏れ防止          | `props` で明示できる            |
| ✅ 保守性が高い          | プロパティ追加もラクラク              |
| ✅ 状態管理と相性◎        | Bloc / Riverpodでの再描画制御に便利 |
| ✅ `toString()`も楽！ | `stringify = true` で自動生成  |