MultiTimer — Clean Architecture + Drift で複数タイマーを実装
| 開発記録 | MultiTimer
タグ: #Flutter #Clean Architecture #Drift
煮込み 15 分と焼き 5 分を同時に走らせたいのに、標準のタイマーは 1 つしか動きません。そこを解決したくて作ったのが MultiTimer です。Clean Architecture で Domain / Data / Presentation の 3 層に分け、下から積み上げた手順を共有します。
複数タイマーが必要な理由
料理中、煮込みと焼きが並行する場面はよくあります。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 の基本です(タイマーアプリに 3 層は大げさかな、とはちょっと思いました)。
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 を意識しすぎず宣言的に書けるのが楽
シンプルなアプリほど、設計を真面目にやると後が楽になります。下ごしらえに使った時間は、機能を足すときに全部返ってきました。