MultiTimer — Clean Architecture + Drift で複数タイマーを実装

| 開発記録 | MultiTimer

タグ: #Flutter #Clean Architecture #Drift

「料理用のタイマーを複数同時に動かしたい」というニーズに応えるアプリを、Clean Architecture でレイヤー分離して実装しました。Domain / Data / Presentation の整理を共有します。

複数タイマーが必要な理由

料理中、煮込みと焼きが並行する場面はよくあります。15 分タイマーと 5 分タイマーを同時に動かしたいのに、標準のタイマーは 1 つしか動かない。

MultiTimer は、複数のタイマーを名前付きで同時に管理できるアプリです。

レイヤー構成

Clean Architecture に従って、3 層に分けました。

  • Domain: Entity(Timer, TimerPreset)、Repository インターフェース、UseCase
  • Data: Drift(SQLite)による永続化、Repository 実装
  • Presentation: UI Widget、Page、Provider

コミット履歴を見ると、feat(domain) → feat(data) → feat(presentation) の順で積み上げています。下から積むのが Clean Architecture の基本です。

Domain 層

class Timer {
  final String id;
  final String name;
  final Duration duration;
  final DateTime? startedAt;
  final TimerStatus status;
}

UseCase は StartTimerUseCase, StopTimerUseCase, ResetTimerUseCase などのシングルアクションで切り出しました。テストを書くのが楽になります。

Domain エンティティは Drift から完全に独立させ、永続化型 (TimerEntity) との変換は Repository 実装の中で完結させます。これでテスト時に Drift をモックしなくて済みます。

class StartTimerUseCase {
  StartTimerUseCase(this._repo, this._clock);
  final TimerRepository _repo;
  final Clock _clock;

  Future<Timer> call(String id) async {
    final current = await _repo.findById(id);
    if (current == null) {
      throw StateError('Timer not found: $id');
    }
    final updated = current.copyWith(
      status: TimerStatus.running,
      startedAt: _clock.now(),
    );
    await _repo.save(updated);
    return updated;
  }
}

Data 層: Drift

Drift で SQLite に永続化しています。アプリを終了してもタイマーの状態が残り、再起動時に「あと 3 分残っていたタイマー」もそのまま続行できます。

@DataClassName('TimerEntity')
class Timers extends Table {
  TextColumn get id => text()();
  TextColumn get name => text()();
  IntColumn get durationMs => integer()();
  DateTimeColumn get startedAt => dateTime().nullable()();
}

Presentation 層

タイマー一覧と詳細を分け、一覧では各タイマーの残り時間をリアルタイム表示します。Provider で状態管理し、1 秒ごとに残り時間を再計算する設計です。

バックグラウンドと通知

アプリがバックグラウンドにいる時もタイマーを正確に管理するため、flutter_local_notifications で通知をスケジュールしています。タイマー満了時に通知が飛びます。

教訓

  • 並列処理が肝のアプリは、永続化を早めに入れる
  • Clean Architecture はレイヤー名にこだわらず、依存方向だけ守る
  • Drift は SQL を意識しすぎず宣言的に書けるのが楽

シンプルなアプリほど、設計を真面目にやると後が楽になります。