Flutterで4層クリーンアーキテクチャを構築するTodoアプリの作り方

## はじめに
Todoアプリを4層のクリーンアーキテクチャで実装しました。
Flutter, Riverpod, Firebaseを使用し、各層の債務を明確に分離した構成になっています。

## できること
- クリーンアーキテクチャの設計に沿ったTodoアプリの実装
- 状態管理はRiverpod, データ保存にはFirebaseを使用
- add / fetch / update / delete の基本機能に対応

## 目次
- [クリーンアーキテクチャとは？](#クリーンアーキテクチャとは？)
- [ディレクトリ構成](#ディレクトリ構成)
- [Flutter create](#flutter-create)
- [Presentation層](#presentation層)
- [Application層](#application層)
- [Domain層](#domain層)
- [Infrastructure層](#infrastructure層)
- [実装まとめ](#実装まとめ)

### 番外編
- [ControllerとUsecaseの違い](#controllerとusecaseの違い)
- [入力欄のバリデーションの責任分担](#入力欄のバリデーションの責任分担)
- [Firestoreのデータ整形はどこでやる？](#Firestoreのデータ整形はどこでやる？)
- [デフォルト値の補完はどの層？](#デフォルト値の補完はどの層？)
- [Formatter関数はどこに書く？](#Formatter関数はどこに書く？)
- [ViewModelは必要か？](#ViewModelは必要か？)



## 参考コード
今回実装した参考コードになります。

https://github.com/life-with-meat/flutter_todo_clean_arch_riverpod/tree/releases/clean-arch-base

## クリーンアーキテクチャとは？
アプリの「責務（役割）」を層に分けて整理し、変更に強く・テストしやすくする設計の考え方。

| 層                  | 役割                   | 例                                              |
| ------------------ | -------------------- | ---------------------------------------------- |
| **Presentation**   | ユーザーに見せる/UI（画面・状態管理） | `Widget`, `StateNotifier`                      |
| **Application**    | ユースケース（何をしたいのか）      | `addTodoUsecase`, `fetchTodoUsecase`           |
| **Domain**         | アプリの本質的なルールやデータ構造    | `TodoModel`, `TodoRepository`（インターフェース）        |
| **Infrastructure** | 外部サービスとのやりとり         | `FirebaseTodoDataSource`, `TodoRepositoryImpl` |


## ディレクトリ構成
クリーンアーキテクチャの4層構造で設計しました。

<details>
<summary>作成したディレクトリ構成</summary>


```
.
├── feature
│   └── todo
│       ├── application
│       │   └── todo_usecases.dart
│       ├── domain
│       │   ├── model
│       │   │   ├── todo_model.dart
│       │   │   ├── todo_model.freezed.dart
│       │   │   └── todo_model.g.dart
│       │   └── todo_repository.dart
│       ├── infrastructure
│       │   ├── data_source
│       │   │   └── firebase_todo_data_source.dart
│       │   └── todo_repository_impl.dart
│       ├── presentation
│       │   ├── controller
│       │   │   └── todo_controller.dart
│       │   ├── todo_page.dart
│       │   └── widget
│       │       ├── app_text_field.dart
│       │       └── todo_dialog.dart
│       └── todo_providers.dart
├── firebase_options.dart
└── main.dart
```

</details>


## Flutter create

```
flutter create --platforms=android,ios --empty my_app
```

## Presentation層
UIと状態管理（StateNotifier）を定義します。

- page
    - TodoPage
- widget
    - AppTextFiled
    - AppDialog
- controller
    - TodoController
        - StateNotifierを使用した状態管理

## Application層
機能ごとのユースケースを定義します。

- TodoUsecases
    - Todoのユースケースを作成
        - fetch
        - add
        - update
        - delete


## Domain層
アプリの本質的なルール(モデル抽象リポジトリ)を定義します。

- TodoModel
    - freezedを使用
- Todorepository
    - 抽象リポジトリ

## Infrastructure層
Firebaseとの通信やデータの取得・保存などの実装を行います。

- datasource
    - FirebaseTodoDataSource
        - Firestoreからのデータ取得・保存を担当
- repositoryImpl
    - TodoRepositoryImpl
        - TodoRepository を実装し、データソースを呼び出す層



## 実装まとめ
責務を分担することで実装が明確になり、理解も深まります。
Todoアプリを作成するだけなら、オーバーエンジニアリングだと感じます。




# 番外

## ControllerとUsecaseの違い

| 層                             | 役割               | 書く内容                                    |
| ----------------------------- | ---------------- | --------------------------------------- |
| **Controller（Presentation層）** | UI のための状態管理と操作受付 | ユースケースの呼び出し、画面状態の更新（ローディング・エラーなど）       |
| **Usecase（Application層）**     | アプリの機能単位の処理フロー   | ビジネスロジックをまとめる。Repositoryの呼び出し、バリデーションなど |


## 入力欄のバリデーションの責任分担

### Controller（UIに近い層）
- ユーザーが入力した瞬間にバリデーション（リアルタイム）
- TextFieldの errorText に表示したり、ボタンの onPressed を制御
- フォーカスアウト時にヒントを表示するなど、UX目的

```
if (title.isEmpty) {
  setState(() => _errorText = 'タイトルを入力してください');
  return;
}
```

###  Usecase（ビジネスルールを守る層）
- アプリの処理として 絶対に許されない値 をチェック

```
Future<void> add(String title) async {
  if (title.trim().isEmpty) {
    throw Exception('タイトルは空にできません');
  }
  ...
}

```

### バリデーションの責任分担

| チェック場所      | 目的         | 実装する場所                      |
| ----------- | ---------- | --------------------------- |
| 入力直後・リアルタイム | UX改善・エラー表示 | Controller（Presentation層） |
| 保存直前・本質的な制約 | データの正当性    | Usecase（Application層）     |



## Firestoreのデータ整形はどこでやる？

Firestoreの生データ（Mapなど）を整形してモデルに変換する処理は、Infrastructure層で実施します。

| 理由                                                | 説明                                          |
| ------------------------------------------------- | ------------------------------------------- |
| Firestoreの形式（例：snake\_case、Timestamp型など）は**外部仕様** | アプリ内部の責務ではなく、外部との接点に閉じ込めるべき                 |
| Domain層やUsecase層は**Firestoreの存在を知らない設計が理想**       | 将来 REST API に変わっても変更はInfrastructureだけで済むように |
| 「整形」は**データ受け取り時の実装の一部**                           | 生→整形済モデルは、外部→内部の典型的な変換処理                    |


## デフォルト値の補完はどの層？

デフォルト値の補完は、原則として Infrastructure層 または Domain層 に書く。

| 目的                     | 書く場所                | 理由・特徴                                    |
| ---------------------- | ------------------- | ---------------------------------------- |
| 🔸 Firestoreの欠損フィールド対策 | **Infrastructure層** | 外部データの整形時に補う（例: `data['title'] ?? '無題'`） |
| 🔸 モデルが持つべき論理的な初期値     | **Domain層**         | アプリの仕様として意味のある初期値（例: `isDone = false`）   |
| 🔸 UI表示用の初期値（フォームなど）   | **Presentation層**   | 状態管理側で補完（例: `TextFieldController` の初期値）  |

## Formatter関数はどこに書く？

Formatter関数は Presentation層 に書く。

### ディレクトリ構成例

```
lib/
└── feature/
    └── todo/
        └── presentation/
            ├── utils/
            │   ├── type_formatter.dart    
            │   └── date_formatter.dart   
```


###  NGな書き方（Domain層やModel内に書く）

```
// NG例: Domain層にUI表示の知識を持たせてしまう
class TodoModel {
  String get typeLabel => type == 1 ? '仕事' : 'その他';
}
```

### まとめ
| 内容              | 書く場所                                    | 理由            |
| --------------- | --------------------------------------- | ------------- |
| enum/type → 表示名 | Presentation/utils/type\_formatter.dart | 表示専用のロジックだから  |
| DateTime → 表示形式 | Presentation/utils/date\_formatter.dart | 日付フォーマットはUI都合 |
| UIの色やアイコン       | ViewModelやFormatterに分離                  | 表示専用の責務       |


## ViewModelは必要か？
FlutterでViewModelは「必須」ではありませんが、UIが複雑になるなら導入するとスッキリします。

```
[TodoModel] (Domain)
      ↓
[StateNotifier] → 状態保持
      ↓
[TodoViewModel] → UI表示用に変換
      ↓
[Widget] → 実際の画面表
```


### TodoViewModel（Presentation層）


<details>
<summary>コードを見る</summary>

```
class TodoViewModel {
  final TodoModel model;

  TodoViewModel(this.model);

  String get typeLabel {
    switch (model.type) {
      case 1: return '仕事';
      case 2: return '勉強';
      default: return 'その他';
    }
  }

  IconData get icon {
    switch (model.type) {
      case 1: return Icons.work;
      case 2: return Icons.school;
      default: return Icons.task;
    }
  }

  Color get color {
    switch (model.type) {
      case 1: return Colors.blue;
      case 2: return Colors.green;
      default: return Colors.grey;
    }
  }

  bool get isDone => model.isDone;
  String get title => model.title;
}

```

</details>


### TodoController（StateNotifier）

<details>
<summary>コードを見る</summary>

```
class TodoController extends StateNotifier<List<TodoModel>> {
  TodoController() : super([]);

  void add(String title, int type) {
    final todo = TodoModel(
      id: DateTime.now().toIso8601String(),
      title: title,
      type: type,
    );
    state = [...state, todo];
  }

  void toggle(String id) {
    state = state.map((t) {
      return t.id == id ? t.copyWith(isDone: !t.isDone) : t;
    }).toList();
  }
}

```

</details>

### UIで ViewModel を使う

<details>
<summary>コードを見る</summary>

```
final todos = ref.watch(todoControllerProvider);

return ListView(
  children: todos.map((todo) {
    final vm = TodoViewModel(todo);
    return ListTile(
      title: Text(vm.title),
      subtitle: Text(vm.typeLabel),
      leading: Icon(vm.icon, color: vm.color),
      trailing: Checkbox(
        value: vm.isDone,
        onChanged: (_) {
          ref.read(todoControllerProvider.notifier).toggle(todo.id);
        },
      ),
    );
  }).toList(),
);

```

</details>