Day Planning
A Flutter daily planner built to validate Clean Architecture + Riverpod patterns outside client constraints — featuring custom gesture-driven timeline UI and offline-first SQLite storage.
Overview
Day Planning is a daily planner app I built as a reference implementation for Clean Architecture and Riverpod outside the constraints of client work. The goal was to validate architecture patterns I wanted to use in production — strict layer separation, immutable state management, and custom rendering — in a controlled, single-owner codebase where every tradeoff was mine to make.
The app focuses on a single day view: time blocking, priority setting, drag-to-reschedule, and progress tracking. No backlog, no calendar, no subscriptions.
Engineering Goals
Most daily planner apps are either too simple (flat to-do lists) or too complex (full project management tools). Rather than building another feature checklist, I set three engineering constraints:
- Strict Clean Architecture layering —
domain/has zero Flutter or I/O dependencies. Presentation never leaks into business logic. Repository contracts sit indomain/, implementations indata/. - Riverpod as the state layer — evaluate how Riverpod's compile-time safety, auto-disposal, and testability hold up against flutter_bloc (my default at the time) outside a team context.
- Custom rendering over dependency — build the timeline UI with
CustomPainterinstead of pulling in a calendar library, to validate whether ~200 lines of painting code could replace a heavy third-party dependency.
My Role
Solo project — architecture, implementation, and Play Store publishing.
Technical Decisions
Architecture: Clean Architecture + Riverpod
The domain layer is pure Dart — no Flutter imports, no framework dependencies. Riverpod StateNotifiers sit between the domain and presentation layers, managing state without leaking framework concepts into business logic:
class TaskNotifier extends StateNotifier<List<Task>> {
final TaskRepository _repository;
TaskNotifier(this._repository) : super([]);
Future<void> loadToday() async {
state = await _repository.getTasksForDate(DateTime.now());
}
Future<void> complete(String taskId) async {
await _repository.markComplete(taskId);
state = state.map((t) =>
t.id == taskId ? t.copyWith(completed: true) : t
).toList();
}
}I chose Riverpod over Bloc for this project because the state graph is narrow (a list of tasks, a selected date, a drag operation flag). Bloc's event → state ceremony would have added boilerplate without proportional benefit at this scale. The takeaway: Riverpod excels in apps with narrow state graphs; Bloc remains the better choice for broad, event-heavy state (like Astute's 20+ features).
Custom Timeline Rendering
The day view renders time blocks on a vertical timeline. Rather than using a third-party calendar/scheduling library, I built the layout with CustomPainter — approximately 200 lines of painting code. This gave full control over the drag gesture math, the 15-minute snap logic, and the visual design, while avoiding the dependency weight and API constraints of a general-purpose calendar widget. The coordinate transformer that maps between scroll-view space and the time axis has since been reused in a professional project.
Offline-First by Design
All data is stored locally with SQLite (via drift). There's no backend — the app works fully offline, and data never leaves the device. This was a deliberate simplification: a daily planner doesn't need a server, and eliminating the network layer removes an entire category of failure states (connectivity, sync conflicts, latency).
Challenges & Solutions
Challenge: Drag-to-reschedule on the timeline needed to snap to 15-minute intervals while feeling fluid. The gesture math — converting pixel offsets to time slots while accounting for scroll position — required several iterations.
Solution: Built a coordinate transformer that maps between the scroll view's coordinate space and the time axis, then added a snapping function that rounds to the nearest 15-minute slot only on gesture end, not during drag. This keeps the drag smooth while landing precisely.
Challenge: Maintaining widget performance with many overlapping time blocks in the timeline view.
Solution: Used RepaintBoundary around each time block widget and const constructors wherever possible. Profiled with Flutter DevTools to confirm the raster thread stayed below 4ms per frame on a mid-range device.
Outcome
- Validated that Clean Architecture + Riverpod produces testable, maintainable code without Bloc's event boilerplate for narrow-state applications
- Confirmed that
CustomPaintercan replace general-purpose calendar libraries for specialized UI — saving ~50KB in dependency size per project - The coordinate transformer pattern and offline-first SQLite layer proved solid enough to extract and reuse in later professional projects (including Surovi Agro Nexus+)
- Published on the Play Store as a working reference implementation of these patterns, not just a design exercise