Skip to content

polatolu/Sliide

Repository files navigation

Sliide

Kotlin Multiplatform (KMP) app for Android and iOS with a shared Compose UI. It provides a user feed backed by the GoRest GraphQL API, with offline-first storage and sync.

How this was built

This app is developed by a human and accelerated by AI. The workflow: developer architected (specs, structure, and design decisions); AI implemented (code from those specs); developer verified and iterated (review, tests, and fixes); AI helped on improvements (polish, refactors, and features like the fancy feed section). Human in the loop, AI in the flow.

If I had more time (Spent around 6 hours)

  • Animations - Richer, smoother transitions (list insertions, detail expand/collapse, morphing FAB) and more polished micro-interactions.
  • Pagination - Load more users using GraphQL pageInfo.endCursor and infinite scroll or “Load more”.
  • Pull-to-refresh - Explicit sync trigger from the feed instead of (or in addition to) the toolbar button.
  • Tests & coverage - More unit tests (e.g. add-user and validation).
  • Accessibility & polish - Screen-reader labels, focus order, and clearer error states.
  • Localizations - Add new locales to all apps.
  • iOS Glass Design Support - Add new iOS Glass Design support by adding SwiftUI in iosMain.

Getting started

1. API token (required for mutations)

The app uses the GoRest API. Create and sync work without a token; add user and delete user require a Bearer token.

  1. Copy the example properties file (do not commit your real token):
    cp local.properties.example local.properties
  2. Edit local.properties in the project root and set your token:
    GOREST_TOKEN=your_bearer_token_here
    Get a free token at https://gorest.co.in/ (sign up and create an API token).
  3. Keep local.properties out of version control. It is listed in .gitignore; never add it to the repo.

The BuildKonfig plugin reads GOREST_TOKEN from local.properties at build time and exposes it as BuildKonfig.GOREST_TOKEN in code. The Ktor client adds it as a Bearer header only for mutations (create/delete user).

2. Build and run

  • Android: Open the project in Android Studio and run the composeApp Android configuration, or from the root:
    ./gradlew :composeApp:installDebug
  • iOS: Build and run the iOS app from Xcode (open the iosApp or generated Xcode project), or use the Kotlin task that builds the framework and then run from Xcode.

Requirements: JDK 11+, Android Studio or IntelliJ, and for iOS: Xcode and a Mac.


Architecture

The app follows Clean Architecture with MVI (Model–View–Intent) on the UI side.

Layers

Layer Role
UI Compose Multiplatform screens and components. A single ViewModel exposes StateFlow<ViewState> and handles Intents (user actions). No business logic in composables.
Domain Use cases only: GetUserFeedUseCase, AddUserUseCase, DeleteUserUseCase. They depend on the repository interface and orchestrate sync/add/delete.
Data Repository implementation (UserRepositoryImpl) that talks to SQLDelight (local DB) and the GraphQL API (Ktor). Single source of truth is the database; network updates the DB, and the UI observes DB flows.

Architecture choices

  • Offline-first: All reads come from SQLDelight. The GraphQL API is used to sync data into the DB and to perform mutations; the UI always reflects local state.
  • Single source of truth: The database. Sync and mutations write to the DB; the repository exposes Flows so the UI updates automatically when data changes.
  • MVI: One-way data flow: Intents → ViewModel → Use cases/Repository → updated State → UI. Makes state predictable and testable.
  • Dependency injection: Koin in shared code. Platform-specific modules provide the SQLDelight driver and any platform APIs; the rest is shared (repository, use cases, ViewModel, GraphQL client).

Current setup

Tech stack

  • Kotlin Multiplatform — shared logic and UI; targets Android and iOS.
  • Compose Multiplatform — single UI codebase (Material 3, adaptive layouts).
  • Ktor — HTTP client for GraphQL (POST with JSON query and variables). Endpoint: https://gorest.co.in/public/v2/graphql.
  • SQLDelight — local SQL DB and type-safe queries; drivers: Android (Android Driver), iOS (Native).
  • Koin — DI for ViewModel, repository, use cases, HTTP client, and DB.
  • BuildKonfig — reads GOREST_TOKEN from local.properties and generates BuildKonfig.GOREST_TOKEN (no token in source).
  • kotlinx.serialization — JSON for GraphQL request/response bodies.

Main modules / packages

  • composeApp — the KMP module (shared + androidMain + iosMain).
  • presentation.feed — User feed screen, list/detail, add-user form, delete confirmation; UserFeedState/UserFeedIntent and SharedViewModel.
  • domain.usecase — GetUserFeed, AddUser, DeleteUser.
  • data.repositoryUserRepository contract and UserRepositoryImpl (DB + API).
  • data.remote — GraphQL API (Ktor), request/response models, BuildKonfig for auth.
  • db — SQLDelight schema and generated code (UserEntity, queries).
  • di — Koin modules (shared + platform).

Features

  • User feed: List and detail view; pull/sync from GraphQL; shimmer while loading; relative timestamps (“5 mins ago”) from syncedAt.
  • Add user: Form with validation (RFC 5322–style email, name min 2 chars); Save disabled until valid; mutation uses Bearer token.
  • Delete user: Long-press → confirm dialog; undo with Snackbar and timeout; then commit deletion to API and DB.

Testing

All tests are unit tests in the shared commonTest source set. They run when you execute the Android unit-test task (shared code is exercised on the JVM as part of that run).

What is tested

Test class What it covers
SharedViewModelTest MVI state and intents: initial state, state from repository, ClearError intent, DeleteUser failure path, relative-time tick flow. Uses FakeUserRepository and Turbine for StateFlow.
GetUserFeedUseCaseTest GetUserFeedUseCase: usersFlow() emits from repository; sync() returns repository result.
RelativeTimeTest formatRelativeTime(): "Just now" (< 1 min), "1 min ago", "N mins ago", "1 hour ago", and future timestamps (clamped to "Just now").
ComposeAppCommonTest Smoke test (e.g. 1 + 2 == 3) to confirm test runtime works.

Total: 13 tests across 4 classes.

Tools

  • kotlin.test — assertions and @Test.
  • kotlinx.coroutines.testrunTest for coroutine-based code.
  • Turbine — testing Flow/StateFlow (e.g. vm.state.test { ... }).
  • FakeUserRepository — in-memory implementation of UserRepository with configurable sync/add/delete results for ViewModel and use-case tests.

Test coverage

JaCoCo is enabled for unit tests. Coverage is collected when you run testDebugUnitTest and reported by the jacocoTestReport task.

Metric Coverage
Instructions 5%
Branches 3%
Lines ~7% (75 of 1,091 lines covered)

Coverage is focused on domain and presentation logic (ViewModel, use cases, RelativeTime); the repository and API are faked in tests. UI composables, DI, theme, and generated code are largely uncovered. Packages with higher coverage in the report: domain.util (e.g. RelativeTime, validation) and domain.usecase (use cases exercised by ViewModel tests).

Generate the report: Run unit tests, then the report task (the report uses the latest coverage data from the last test run):

./gradlew :composeApp:testDebugUnitTest
./gradlew :composeApp:jacocoTestReport

Open the HTML report at composeApp/build/reports/jacoco/jacocoTestReport/html/index.html to see per-package and per-class coverage.

How to run

From the project root:

./gradlew :composeApp:testDebugUnitTest

This builds the composeApp module and runs all unit tests (including commonTest). For other targets, use the matching test task (e.g. testReleaseUnitTest).

Test results

Run the command above locally to get current results. Example output when all tests pass:

> Task :composeApp:testDebugUnitTest
ComposeAppCommonTest > example() PASSED
GetUserFeedUseCaseTest > usersFlow_emitsFromRepository() PASSED
GetUserFeedUseCaseTest > sync_returnsRepositoryResult() PASSED
RelativeTimeTest > formatRelativeTime_justNow_underOneMinute() PASSED
RelativeTimeTest > formatRelativeTime_oneMinuteAgo() PASSED
RelativeTimeTest > formatRelativeTime_minsAgo() PASSED
RelativeTimeTest > formatRelativeTime_oneHourAgo() PASSED
RelativeTimeTest > formatRelativeTime_future_returnsJustNow() PASSED
SharedViewModelTest > initialState_hasEmptyUsers() PASSED
SharedViewModelTest > state_emitsUsersFromRepository() PASSED
SharedViewModelTest > intentClearError_clearsErrorMessage() PASSED
SharedViewModelTest > intentDeleteUser_onFailure_setsErrorMessage() PASSED
SharedViewModelTest > relativeTimeTickFlow_emits() PASSED

BUILD SUCCESSFUL

HTML reports are written to composeApp/build/reports/tests/testDebugUnitTest/ after a run.


Summary for new developers

  1. Copy local.properties.example to local.properties and set GOREST_TOKEN (see Getting started).
  2. Architecture: Clean Architecture + MVI; UI → ViewModel → Use cases → Repository; DB is the single source of truth; Koin for DI.
  3. Stack: KMP, Compose, Ktor (GraphQL), SQLDelight, BuildKonfig, Koin.
  4. Where to look: Feed UI and intents in presentation.feed; business flow in domain.usecase and data.repository; API and models in data.remote; schema in db and composeApp/.../sqldelight.

About

Sliide KMP Challenge

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors