Tickr is an offline-first Android task management app built with Kotlin and Jetpack Compose to demonstrate production-style Android architecture.
It is designed as an interview-ready project that showcases how to build a local-first app with Room as the single source of truth, reactive UI using Flow/StateFlow, and resilient background sync using WorkManager.
- Kotlin
- Jetpack Compose
- MVVM
- Room
- Repository Pattern
- Flow / StateFlow
- WorkManager
- Material 3
- Create, edit, complete, and delete tasks
- Filter tasks by All, Active, and Completed
- Offline-first local persistence with Room
- Reactive task streams using Flow
- Sync states for pending, syncing, synced, and failed operations
- Soft delete flow for sync-safe task deletion
- Background retryable sync using WorkManager
- Fake remote data source to simulate production-style sync behavior
- Due date picker UI for task reminders/planning
Tickr was built to practice and demonstrate:
- offline-first data flow
- clean Android architecture
- local database as single source of truth
- repository-driven state transitions
- background work and retry handling
- interview-friendly design decisions that are easy to explain
Tickr follows a layered architecture:
UI (Compose)
-> ViewModel
-> Repository
-> Room (single source of truth)
-> Remote sync layer
-> WorkManager worker
uiCompose screens, UI state models, and ViewModelsdomainDomain models and repository contractsdataRoom entities, DAO, repository implementation, mappers, and fake remote data sourceworkerBackground sync scheduling and worker execution
- UI collects
StateFlowfrom the ViewModel - ViewModel observes tasks from the repository
- Repository reads from Room using
Flow - Room emits updates whenever the local database changes
- Compose recomposes from local state
- User creates/updates/deletes a task
- Repository writes the change to Room immediately
- Task is marked with a sync state such as pending create/update/delete
- UI updates instantly from the local database
- WorkManager later triggers background sync
- Sync success updates the task as
SYNCEDor removes soft-deleted rows - Sync failure marks the task as failed so it can be retried
Tickr separates internal sync bookkeeping from UI-facing sync state.
PENDING_CREATEPENDING_UPDATEPENDING_DELETESYNCINGSYNCEDFAILED_CREATEFAILED_UPDATEFAILED_DELETE
PENDINGSYNCINGSYNCEDFAILED
This keeps repository logic expressive while keeping UI state simple.
app/src/main/java/com/rohitkhandelwal/tickr
├── core
├── data
│ ├── local
│ ├── mapper
│ ├── remote
│ └── repository
├── di
├── domain
├── ui
│ ├── navigation
│ └── screen
└── worker
Tickr currently uses a fake remote data source to simulate production-style sync without requiring a real backend.
You can intentionally trigger sync failures by adding:
[fail-sync]
to a task title or description. This is useful for demonstrating:
- retry behavior
- failed sync states
- local-first UX under unreliable network conditions
- Android Studio
- JDK 11+
- Android SDK with a recent emulator or physical device
- Clone the repository
- Open the project in Android Studio
- Let Gradle sync finish
- Run the
appconfiguration on an emulator or device
- How to design an offline-first Android app
- Why Room should be the single source of truth
- How MVVM, repository pattern, and Flow work together
- How to model sync states and retries cleanly
- When to use WorkManager for resilient background work
- How to keep architecture production-style without overengineering
- Real backend integration
- Exact recurring reminders using AlarmManager
- Hilt for dependency injection
- UI tests for major flows
- Search and sorting improvements
- Multi-module modularization if the project grows
Rohit Khandelwal
- GitHub: rohitk2001




