Android learning app that demonstrates offline-first architecture and data synchronization.
User actions always write to Room first. The UI observes Room only. A fake backend is updated later by WorkManager. There is no real HTTP server and no Retrofit.
Compose UI
↓
ViewModel (StateFlow<TodoUiState>)
↓
Use cases → Repository
↓
Room ← Single Source of Truth
↓
TodoSyncWorker
↓
FakeRemoteDataSource
- Add, complete, and delete todos
- Use the app with no internet
- See local changes immediately
- Watch pending rows sync when connectivity (real or simulated) returns
- Force server success, server error, or slow responses from the Debug screen
| Layer | Library |
|---|---|
| Language | Kotlin |
| UI | Jetpack Compose, Material 3 |
| Architecture | MVVM |
| Async | Coroutines, Flow |
| Local DB | Room |
| DI | Hilt |
| Background sync | WorkManager |
| Network awareness | ConnectivityManager / NetworkCallback |
Primary color: #6200EE (Material Purple 500). Dynamic/wallpaper colors are disabled so the purple theme stays consistent.
- Android Studio that supports Android Gradle Plugin 9.3 (recent Otter / Narwhal or newer)
- JDK 17 (Studio’s bundled JDK is fine)
- Android SDK with compile SDK 37 installed
- An emulator or device on API 24+
This project uses AGP 9’s built-in Kotlin. You do not apply org.jetbrains.kotlin.android yourself.
- Clone or copy the project and open the root folder (
OfflineTodoApp) in Android Studio. - Let Gradle sync. The first sync downloads the Gradle 9.5 wrapper and dependencies.
- If sync fails on Hilt / KSP:
- Hilt 2.60.1 is required for AGP 9 (
BaseExtensionwas removed in older Hilt plugins). gradle.propertiessetsandroid.disallowKotlinSourceSets=falseso KSP 2.2.x can register generated sources under built-in Kotlin.
- Hilt 2.60.1 is required for AGP 9 (
- Create or select a virtual device (API 24 or higher).
- Run the app configuration (green Run button) or:
./gradlew :app:installDebugOn Windows use gradlew.bat.
- The user adds, toggles, or deletes a todo.
TodoRepositorywrites Room immediately withsyncStatus = PENDING.- Compose collects
TodoDao.observeVisible()— the list updates without waiting on the network. SyncSchedulerenqueues uniqueTodoSyncWorkerwork (NetworkType.CONNECTED).- The worker calls
FakeRemoteDataSourceone pending row at a time:- Success:
SYNCED, or a hard delete after a confirmed soft-delete - No network: rows stay
PENDING, worker returnsResult.retry() - Server error: row becomes
FAILED(Retry on the row)
- Success:
Sync is entity state, not an operation log. Offline “add A, then complete A” is still one row whose latest fields are pushed.
Deletes are soft (isDeleted = true) until the fake server confirms, then the row is removed from Room.
Open Debug in the top bar.
| Control | What it does |
|---|---|
| Network ON / OFF | Fake API + the “Network:” banner. Does not toggle airplane mode. |
| Server Success | Normal ~400 ms call |
| Server Error | Marks the item Failed |
| Slow Response | ~4 s delay |
| Sync logs | In-app copy of TODO_CREATED, SYNC_* events |
The banner is ONLINE only when the device radio and the simulator are both on.
Filter Logcat by tag OfflineTodo.
- Debug → Network OFF
- Add
Learn Kotlin— it appears as Pending - Network ON — status becomes Synced
- Network OFF
- Add A, add B, complete A, delete B
- Network ON — A is completed and synced; B is purged after the fake delete
- Server Error
- Add a todo (or tap Sync) — status Failed
- Switch to Server Success, tap Retry
app/src/main/java/com/offlinetodo/app/
├── data/
│ ├── local/ TodoEntity, TodoDao, TodoDatabase
│ ├── remote/ FakeRemoteDataSource, NetworkSimulator
│ └── repository/ TodoRepository
├── domain/
│ ├── model/ Todo, SyncStatus
│ └── usecase/ AddTodo, ToggleTodo, DeleteTodo, SyncTodos
├── presentation/ TodoScreen, TodoViewModel
│ ├── components/ Shared scaffold + gradient chrome
│ └── debug/ DebugScreen
├── worker/ TodoSyncWorker, SyncScheduler, SyncTrigger
├── di/ AppModule
└── util/ ConnectivityObserver, AppLog
Light theme primary is #6200EE. Screens sit on a vertical purple gradient that fades into the surface color. Layout and actions (add, checkbox, delete, retry, Debug, Sync) are unchanged; the gradient is background only.
./gradlew :app:assembleDebugWorkManager is initialized through OfflineTodoApplication (Configuration.Provider + HiltWorkerFactory). The default WorkManager InitializationProvider is removed in the manifest so Hilt can inject TodoSyncWorker.