Skip to content

Repository files navigation

📱 Kotlin Multiplatform User List — Offline-First with Clean Architecture

A Kotlin Multiplatform for practising offline-online sync2 (save locally → auto-sync when online → retry in background), rebuilt with clean architecture and MVVM. The shared module targets Android + iOS; the runnable app here is Android (Jetpack Compose). iOS actuals are stubbed pending an iOS app target.

Save users locally → auto-sync to server when online → retry failed syncs in background.


🏗 Tech Stack

Layer Technology
UI Jetpack Compose + Material 3 (Android)
Architecture Clean Architecture (data / domain / presentation) + MVVM
DI Koin
Local DB SQLDelight
Networking Ktor Client
Background Sync WorkManager (Android)
Connectivity ConnectivityManager via a shared NetworkInfo abstraction
Error Handling A small Either<Failure, T> type (dartz-style)

🔄 How It Works

User taps "Add" → ViewModel validates input
                        │
                        ▼
                  UseCase → Repository.addUser()
                    ┌────┴────┐
                    ▼         ▼
              SQLDelight    Online?
              INSERT          │
             (isSynced=0)┌────┴────┐
                    │    YES       NO
                    │    │         │
                    │  POST →   SyncScheduler
                    │  mark     retries when
                    │  synced   network returns
                    ▼
             Flow<List> emits → UI recomposes automatically

Key behaviour:

  • Data is always saved locally first (SQLDelight/SQLite), making the app fully usable offline
  • If online, an immediate POST is attempted; on success the row is marked as synced
  • If offline or the POST fails, the row stays isSynced = false
  • Real-time connectivity monitoring via ConnectivityManager.NetworkCallback — the UI reacts instantly
  • When connectivity returns, an immediate sync pushes all pending rows
  • WorkManager runs a periodic sync (every 15 min) as a safety net
  • The UI shows "Synced" / "Pending" badges and an offline banner

📁 Project Structure

kmp_user_list/
├── settings.gradle.kts
├── build.gradle.kts
│
├── shared/                                     ← Kotlin Multiplatform module (androidTarget + iOS)
│   └── src/
│       ├── commonMain/kotlin/com/potential/kmpuserlist/
│       │   ├── core/
│       │   │   ├── platform/PlatformContext.kt         ← expect Context abstraction
│       │   │   ├── database/DatabaseDriverFactory.kt   ← expect SQLDelight driver factory
│       │   │   ├── network/NetworkInfo.kt              ← expect connectivity monitor
│       │   │   ├── network/HttpClientFactory.kt         ← Ktor client (expect engine)
│       │   │   ├── worker/SyncScheduler.kt              ← expect background sync scheduler
│       │   │   ├── usecase/UseCase.kt                   ← base UseCase<T, Params> contract
│       │   │   ├── error/Failure.kt
│       │   │   ├── util/Either.kt
│       │   │   └── di/AppModule.kt                      ← shared Koin modules
│       │   │
│       │   └── features/user_list/
│       │       ├── data/
│       │       │   ├── model/UserDto.kt                 ← Ktor/JSON DTO
│       │       │   ├── source/UserLocalSource.kt         ← SQLDelight CRUD
│       │       │   ├── source/UserRemoteSource.kt         ← Ktor API calls
│       │       │   └── repository/UserRepositoryImpl.kt   ← offline-first implementation
│       │       │
│       │       ├── domain/
│       │       │   ├── entity/UserEntity.kt
│       │       │   ├── repository/UserRepository.kt
│       │       │   └── usecase/
│       │       │       ├── AddUserUseCase.kt
│       │       │       ├── DeleteUserUseCase.kt
│       │       │       ├── GetUsersUseCase.kt
│       │       │       └── SyncUsersUseCase.kt
│       │       │
│       │       └── presentation/
│       │           ├── UserListViewModel.kt              ← plain MVVM view-model + StateFlow
│       │           └── UserListState.kt
│       │
│       ├── commonMain/sqldelight/.../database/User.sq   ← table + queries
│       ├── androidMain/kotlin/...                        ← Android actuals (WorkManager, ConnectivityManager, AndroidSqliteDriver, OkHttp)
│       └── iosMain/kotlin/...                            ← iOS actuals (stubbed; see note below)
│
└── androidApp/                                 ← Android application (Jetpack Compose)
    └── src/main/kotlin/com/potential/kmpuserlist/android/
        ├── UserListApplication.kt              ← Koin startup + periodic sync scheduling
        ├── MainActivity.kt
        ├── AndroidUserListViewModel.kt          ← androidx.lifecycle.ViewModel adapter
        └── ui/
            ├── UserListScreen.kt                ← home screen (offline banner, list, FAB)
            ├── AddUserDialog.kt                 ← input dialog with validation
            └── UserItem.kt                      ← card with sync badge + delete

✨ Features

  • Add User — name (letters only) and age (digits only) with input validation
  • Delete User — remove entries with a single tap
  • Offline Banner — appears/disappears instantly based on real-time connectivity
  • Sync Status Badges — each row shows "Synced" or "Pending"
  • Auto-Sync on Reconnect — pending rows are pushed immediately when internet returns
  • Background Sync — WorkManager retries every 15 min as a safety net
  • Material 3 — supports light/dark theme with system preference

🚀 Getting Started

cd kmp_user_list
./gradlew :androidApp:installDebug

Note: The project uses jsonplaceholder.typicode.com as a mock API (see UserRemoteSource). Swap the base URL for your real endpoint when wiring up production data.


🧪 Testing Offline Sync

  1. Add a few users with internet ON → they show "Synced"
  2. Turn on Airplane Mode → offline banner appears instantly
  3. Add more users → they show "Pending"
  4. Turn Airplane Mode OFF → banner disappears, pending rows sync automatically

This exact flow was verified end-to-end on an emulator during development (add → sync, delete, offline add → "Pending", reconnect → auto-sync to "Synced").


📌 Notes on the iOS target

shared declares iosX64, iosArm64, and iosSimulatorArm64 targets and compiles cleanly for all three, but NetworkInfo and SyncScheduler iOS actuals are intentionally minimal stubs (always-online, no-op scheduling) since only the Android app was requested/verified here. Swap them for NWPathMonitor / BGTaskScheduler implementations when an iOS app target is added — DatabaseDriverFactory and the Ktor Darwin engine are already wired up for that target.


✅ Production Checklist

  • Offline-first with SQLDelight + isSynced flag
  • Clean Architecture (data / domain / presentation)
  • Immediate sync attempt on add
  • Real-time connectivity monitoring
  • Auto-sync when connectivity returns
  • Periodic background sync via WorkManager
  • MVVM presentation layer (StateFlow-based)
  • Koin dependency injection
  • Ktor HTTP client with logging + timeouts
  • Input validation (letters-only name, digits-only age)
  • Delete user support
  • Either-based error handling
  • Material 3 + light/dark theme
  • Reactive UI via Flow
  • Unit tests (Repository, ViewModel)
  • Pull-to-refresh
  • Edit user functionality
  • iOS app target (shared module is ready; actuals are stubbed)

About

A KMP practise project to lean KMP & practise offline/online sync

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages