A polished, offline-first task tracker built with Flutter, showcasing clean architecture, reactive data flow, and thoughtful UX design. This is a portfolio project demonstrating senior-level Flutter development practices.
A task management app that prioritizes simplicity and speed. Designed for users who want to quickly capture tasks, organize them with tags, and track completion without friction.
Key Principles:
- Offline-first: All data stored locally via SQLite (Drift)
- Reactive: Real-time updates as tasks change
- Responsive: Adaptive layout for phones and tablets
- Polished: Smooth animations and thoughtful interactions
lib/
├── core/ # Routing, theming, constants
├── domain/ # Business logic (pure Dart)
│ ├── tasks/ # Task model, filters, lifecycle
│ └── tags/ # Tag repository interface
├── data/ # Data layer
│ ├── database/ # Drift schema & provider
│ ├── tasks/ # Task repository implementations
│ └── tags/ # Tag repository implementations
└── features/ # UI layer
├── tasks/ # Task list, editor, detail view
└── tags/ # Tag input field
flowchart TD
A[UI Widgets] -->|watch| B[Riverpod Providers]
B -->|query| C[Repository]
C -->|watchAll| D[Drift Database]
D -->|Stream| C
C -->|Stream| B
B -->|notify| A
- UI watches providers via
ref.watch() - Providers query repositories for data streams
- Repositories return
Stream<T>from Drift - Drift emits updates on database changes
- UI rebuilds automatically with new data
Why Riverpod?
- Compile-safe: No runtime exceptions from missing providers
- Testable: Easy to override with fakes in tests
- Reactive: Built-in stream support for real-time updates
- Scoped: Providers live with the widget tree, no global state
Pattern Used:
// Repository provider (injected)
final taskRepositoryProvider = Provider<TaskRepository>((ref) => ...);
// Stream provider (reactive)
final allTasksProvider = StreamProvider<List<Task>>((ref) {
return ref.watch(taskRepositoryProvider).watchAll();
});
// Filtered provider (derived state)
final filteredTasksProvider = Provider<AsyncValue<List<Task>>>((ref) {
final filter = ref.watch(taskFilterProvider);
final tasks = ref.watch(allTasksProvider);
return tasks.whenData((list) => filter.apply(list));
});- Create, edit, delete tasks with title, description, dates, and tags
- Lifecycle states: Active, Scheduled, Completed
- Due date buckets: Overdue, Today, Upcoming
- Multi-tag filtering: AND logic for precise filtering
| Screen Size | Layout |
|---|---|
| < 700px | Single pane with navigation |
| >= 700px | Split view: list (380px) + detail pane |
- Completion bounce: Scale animation on checkmark (1.0 → 1.3 → 1.0)
- Strikethrough: Smooth text decoration transition
- Selection indicator: Primary color left border on selected task
| Decision | Rationale |
|---|---|
| Drift over Hive/Isar | Stronger architectural signal (SQL + migrations), reactive streams built-in |
| No undo snackbar | Filter change already provides visual feedback; keeps UI uncluttered |
| No swipe gestures | Tap + toggle is faster and more accessible |
| Selection clears on filter | Selection coherence: only show what's in the current list |
| In-memory repos kept | Useful for widget tests (no DB setup) and validates interface design |
| No cloud sync | Scope control: offline-first keeps complexity manageable |
flutter testCoverage:
- 96 tests across 13 test files
- Unit tests: domain logic (filters, lifecycle)
- Integration tests: Drift repositories with in-memory SQLite
- Widget tests: screens, animations, user flows
Test Philosophy:
- Fakes over mocks for repositories (test behavior, not implementation)
- Async providers replay initial state immediately (no
pumpAndSettledelays) - Riverpod
overrideWithValuefor dependency injection in tests
- Local Notifications: Reminders for due dates using
flutter_local_notifications - Import/Export: JSON backup for data portability
- Dark Mode Polish: System-aware theme switching with persistence
- Recurring Tasks: Weekly/monthly task templates
| Layer | Technology |
|---|---|
| State Management | Riverpod |
| Routing | go_router |
| Database | Drift (SQLite) |
| Local Storage | path_provider |
| Code Generation | build_runner, freezed, drift_dev |
| Testing | flutter_test, flutter_riverpod |
- Task list with filters
- Task editor
- Empty states
- List + detail side by side
- Selection indicator
- Real-time filter changes
# Install dependencies
flutter pub get
# Generate code (Drift, Freezed, Riverpod)
flutter pub run build_runner build --delete-conflicting-outputs
# Run tests
flutter test
# Run app
flutter runlib/
├── core/
│ └── routing/
│ ├── app_router.dart # go_router configuration
│ └── app_routes.dart # Route constants
│
├── domain/
│ └── tasks/
│ ├── task.dart # Freezed model with lifecycle
│ ├── task_filter.dart # Filtering logic (AND composition)
│ ├── task_lifecycle.dart # Active/Scheduled/Completed logic
│ └── task_repository.dart # Abstract interface
│
├── data/
│ ├── database/
│ │ ├── database.dart # Drift schema (Tasks, Tags tables)
│ │ └── database_provider.dart
│ ├── tasks/
│ │ ├── drift_task_repository.dart # SQLite implementation
│ │ └── in_memory_task_repository.dart # Test fake
│ └── tags/
│ ├── drift_tag_repository.dart
│ └── in_memory_tag_repository.dart
│
└── features/
└── tasks/
├── presentation/
│ ├── adaptive_task_list_screen.dart # Split/narrow logic
│ ├── task_detail_view.dart # Right pane content
│ ├── task_editor_screen.dart # Create/edit form
│ ├── task_list_screen.dart # Narrow layout list
│ └── widgets/
│ ├── animations.dart # Completion bounce
│ └── task_list_item.dart # List tile with swipe
└── state/
├── task_filter_notifier.dart # Filter state
└── task_providers.dart # Riverpod providers
MIT - Use as a reference for your own Flutter projects.
Built as a portfolio piece. Questions or feedback welcome!