WisePrior is a production-grade Android task manager designed with a heavy focus on Scalability, Modularization, and Quality Engineering. Inspired by modern productivity tools, it serves as a technical showcase for Clean Architecture and robust background synchronization. Open source and available under the MIT License.
| Add reminders — Demo 01 | Add reminders — Demo 03 |
|---|---|
![]() |
![]() |
| List reminders — Light Mode | List reminders — Dark Mode |
|---|---|
![]() |
![]() |
| Filters and Tag manager |
|---|
![]() |
WisePrior is built on Clean Architecture principles, enforcing a strict separation of concerns between business logic, data handling, and UI. This approach was chosen to ensure the codebase remains maintainable as features grow and to enable high-fidelity testing of each component in isolation.
The project utilizes MVVM for UI logic and MVI-inspired state management for predictable, unidirectional data flow, following the Now in Android best practices.
┌────────────────────────────────────────────────────────┐
│ :app │
│ Navigation 3 · Hilt setup · Deep links │
└──────────────┬────────────────────────┬────────────────┘
│ │
┌──────────────▼──────┐ ┌─────────────▼──────────────┐
│ :feature:tasklist │ │ :feature:taskeditor │
│ ViewModel · UI │ │ ViewModel · UI │
└──────────┬──────────┘ └─────────────┬───────────────┘
│ │
└──────────────┬─────────────┘
│
┌─────────────────▼──────────────────────────┐
│ core modules │
│ │
│ :core:domain ←────── :core:model │
│ ↑ │
│ :core:data ←──────── :core:storage │
│ ↑ │
│ :core:notifications (AlarmManager + WM) │
│ │
│ :core:ui ←────────── :core:designsystem │
│ :core:common │
└──────────────────────────────────────────────┘
The project is decomposed into specialized modules, each adhering to the Single Responsibility Principle (SRP). These modular boundaries prevent “spaghetti dependencies” and significantly reduce build times via parallel compilation.
| Module | Responsibility |
|---|---|
:app |
Entry point, Hilt setup, Navigation 3 host, notification deep link handling |
:core:model |
Domain models (Task, Priority, RecurrenceType) — pure Kotlin, zero Android deps |
:core:domain |
TaskRepository interface + use cases: GetTasks, GetTaskById, AddTask, UpdateTask, DeleteTask |
:core:data |
TaskRepositoryImpl, TaskMapper, Hilt RepositoryModule |
:core:storage |
Room database v4, TaskEntity, TaskDao, TypeConverters, schema migrations |
:core:notifications |
AlarmManagerNotificationScheduler, AlarmReceiver, BootReceiver, RescheduleNotificationsWorker, NotificationHelper |
:core:designsystem |
WisePriorTheme, Material You colors, typography, shapes |
:core:ui |
Shared Compose components: TaskCard, ToggleRow, SectionHeader, EmptyState, PriorityBadge |
:core:common |
Coroutine dispatcher qualifiers (@IoDispatcher, @DefaultDispatcher, @MainDispatcher) |
:feature:tasklist |
Task list — TaskListViewModel, swipe-to-delete, animated list, notification cancellation on delete |
:feature:taskeditor |
Task editor — TaskEditorViewModel, date/time pickers, recurrence, notification scheduling on save |
core modules — never on each other.:core:domain defines the repository contract; :core:data implements it.:core:storage has no knowledge of domain models; mapping is handled exclusively in :core:data.:core:notifications depends on :core:model and :core:domain for scheduling logic.| Category | Library | Version |
|---|---|---|
| Language | Kotlin | 2.3.0 |
| UI | Jetpack Compose + Material 3 | BOM 2025.09.01 |
| Architecture | ViewModel + StateFlow (MVI-like UDF) | Lifecycle 2.10.0 |
| Dependency Injection | Hilt | 2.59 |
| Navigation | Navigation 3 (stable) | 1.0.0 |
| Database | Room | 2.8.3 |
| Background | AlarmManager + WorkManager | — / 2.10.0 |
| Annotation Processing | KSP | 2.3.4 |
| Build | AGP 9.0.0 + Gradle 9.1.0 | — |
| Serialization | Kotlinx Serialization | 1.7.3 |
Each task carries: title, notes, URL, due date, time flag, urgent flag, priority (None / Low / Medium / High), tags, flagged marker, and recurrence type (None / Daily / Weekly / Monthly).
LargeTopAppBar with collapse-on-scroll behavior.LazyColumn with stable keys and animateItem() for smooth insert/remove animations.SwipeToDismissBox — automatically cancels associated alarms.ElevatedCard UI with custom priority indicators.FilterChip.AlarmManager.setExactAndAllowWhileIdle() — fires even in Doze modeBootReceiver enqueues a WorkManager job that re-fetches all tasks and restores alarmsenableEdgeToEdge() before setContent, windowSoftInputMode="adjustResize", insets via Scaffold.
Each feature follows strict Unidirectional Data Flow. Navigation is triggered via a Channel<Unit> — never via a boolean in UiState — to guarantee exactly one delivery and prevent flickering.
User action ──▶ ViewModel.onEvent(Event) ──▶ StateFlow<UiState> update ──▶ UI Recompose
└──▶ Channel<Unit>.send(Unit) ──▶ Navigation
At WisePrior, the architecture is the foundation of our testing strategy. By decoupling business rules from Android frameworks, we achieve a high degree of testability where every layer can be validated independently. Our suite provides fast feedback, prevents regressions, and ensures that critical user journeys remain functional across all SDK versions.
The project follows the classic Test Pyramid principle: favour many fast, isolated unit tests at the base; a smaller set of integration tests in the middle; and targeted UI tests at the top for critical user flows.
▲
/ \ UI / End-to-End
/ UI \ (Espresso — critical flows)
/──────\
/ \ Integration
/ Integrat.\ (Room in-memory, Repository + DataSource)
/────────────\
/ \ Unit tests
/ Unit Tests \ (Use Cases, ViewModels, Repositories, Mappers)
/──────────────────\
| Level | Scope | Speed | Cost |
|---|---|---|---|
| Unit | Business Logic, ViewModels, Mappers | ⚡ Milliseconds | Low |
| Integration | Persistence (Room), Repositories | 🕐 Seconds | Medium |
| E2E / UI | Full User Journeys (Compose + Espresso) | 🕑 Minutes | High |
Unit tests target every layer of the Clean Architecture stack — use cases, repositories, mappers, and view models — using fakes and mocks to keep each test fully isolated.
What is covered
| Module | Classes under test |
|---|---|
core:domain |
All 9 use cases: GetTasksUseCase, AddTaskUseCase, UpdateTaskUseCase, DeleteTaskUseCase, GetTaskByIdUseCase, GetTagsUseCase, AddTagUseCase, UpdateTagUseCase, DeleteTagUseCase |
core:data |
TaskRepositoryImpl, TagRepositoryImpl, TaskMapper, TagMapper |
core:storage |
Converters (Room TypeConverter round-trips) |
feature:tasklist |
TaskListViewModel — state emissions, deletion, error handling, all 6 collection filters, tag editor with MAX_TAGS enforcement |
feature:taskeditor |
TaskEditorViewModel — task loading, all TaskEditorEvent types, save validation, date/time preservation, notification scheduling |
Tools
| Tool | Role |
|---|---|
| JUnit 4 | Test runner and base assertions |
| MockK 1.13 | Kotlin-idiomatic mocking — mockk(relaxed = true), slot capture, coVerify |
| Turbine 1.2 | StateFlow / Flow testing: awaitItem(), cancelAndIgnoreRemainingEvents() |
| kotlinx.coroutines.test | runTest, UnconfinedTestDispatcher, Dispatchers.setMain/resetMain |
Naming convention — every test name is self-documenting using the given / when / then format:
@Test
fun `given active ByTag collection, when that tag deleted, then selection falls back to All`()
@Test
fun `given tag count at maximum, when saveTag called for new tag, then error is set and tag is not added`()
Coverage Overview
Key Scenarios
We utilize an in-memory Room database to run integration tests that validate the real data flow from the repository down to the SQLite layer.
Why In-Memory?
End-to-End tests in the :app module exercise the full stack, including Hilt dependency injection and Navigation 3 transitions.
Full Task Lifecycle Test
Stability Features
waitUntil helpers ensure tests wait for async database operations before asserting.master are blocked unless all Unit and UI tests pass.git clone https://github.com/gugabrilhante/WisePrior.git
cd WisePrior
./gradlew assembleDebug
Gustavo Brilhante — gugabrilhante@gmail.com
This project is licensed under the MIT License - see the LICENSE file for details.