3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-13

はじめに

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

できること

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

目次

番外編

参考コード

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

クリーンアーキテクチャとは?

アプリの「責務(役割)」を層に分けて整理し、変更に強く・テストしやすくする設計の考え方。

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

ディレクトリ構成

クリーンアーキテクチャの4層構造で設計しました。

作成したディレクトリ構成
.
├── 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

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層)

コードを見る
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;
}

TodoController(StateNotifier)

コードを見る
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();
  }
}

UIで ViewModel を使う

コードを見る
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(),
);

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?