Claude
Skills
Sign in
Back

compose-arch

Included with Lifetime
$97 forever

Compose Multiplatform Architecture Framework - strict Screen/View/Component layering, use cases, repositories, and feature slice patterns

Design

What this skill does


# Compose Multiplatform Architecture Framework

> **SINGLE SOURCE OF TRUTH** for Compose Multiplatform architecture rules. All agents and skills reference this file — do not duplicate these rules elsewhere.

Strict architectural patterns for building Compose Multiplatform features using feature slices. Enforces separation of concerns through Screen/View/Component layering.

**Related skills:**
- `kmp-feature-slice` — procedural feature generation workflow (uses this skill's rules)
- `kotlin-web` — web frontend patterns (Compose WASM follows these same rules)

## Core Principles

### Layer Separation (STRICT)

| Layer | Responsibility | Rules |
|-------|----------------|-------|
| **Screen** | Thin adapter | Reads viewState, passes to View. NO logic, NO remember, NO calculations |
| **View** | Pure UI | Only layout, only viewState, only eventHandler. NO side effects |
| **Component** | All logic | State, events, use cases, lifecycle. Uses Decompose |
| **Domain** | Business | Use cases, repositories, data sources |

## Screen Layer

**File**: `<FeatureName>Screen.kt`

```kotlin
@Composable
fun FeatureScreen(component: FeatureComponent) {
    val viewState by component.viewState.subscribeAsState()
    FeatureView(viewState, component::obtainEvent)
}
```

### Screen Rules
- **Maximum**: 1000 lines (hard limit)
- **Recommended**: Under 600 lines
- **Forbidden**:
  - Business logic
  - Navigation logic
  - State management
  - `remember` calls
  - Calculations

## View Layer

**File**: `<FeatureName>View.kt`

```kotlin
@Composable
fun FeatureView(
    viewState: FeatureViewState,
    eventHandler: (FeatureEvent) -> Unit
) {
    // Only layout and viewState rendering
    Column(modifier = Modifier.fillMaxSize()) {
        when (viewState) {
            is FeatureViewState.Loading -> LoadingContent()
            is FeatureViewState.Success -> SuccessContent(
                data = viewState.data,
                onItemClick = { eventHandler(FeatureEvent.ItemClicked(it)) }
            )
            is FeatureViewState.Error -> ErrorContent(
                message = viewState.message,
                onRetry = { eventHandler(FeatureEvent.Retry) }
            )
        }
    }
}
```

### View Rules
- Only layout code
- Only work with viewState
- Only call eventHandler
- **NO** logic
- **NO** remember
- **NO** side effects
- **NO** previews in production code

### UI Guidelines
- Maximum nesting depth: **3 levels**
- Spacing: multiples of **8/16/24** dp
- Use theme: `AppTheme.colors`, `AppTheme.typography`
- Use theme icons consistently
- Extract to `common/ui/` if used in **5+ places**

## Component Layer

**File**: `<FeatureName>Component.kt`

```kotlin
interface FeatureComponent {
    val viewState: Value<FeatureViewState>
    fun obtainEvent(event: FeatureEvent)
}

// @AssistedInject — required whenever any constructor parameter is @Assisted.
// Plain @Inject would fail at compile time with "missing binding for ComponentContext".
@AssistedInject
class DefaultFeatureComponent(
    private val getDataUseCase: GetDataUseCase,
    @Assisted componentContext: ComponentContext,
    @Assisted private val onNavigate: (String) -> Unit,
) : FeatureComponent, ComponentContext by componentContext {

    private val _viewState = MutableValue<FeatureViewState>(FeatureViewState.Loading)
    override val viewState: Value<FeatureViewState> = _viewState

    private val scope = componentScope()

    init { loadData() }

    override fun obtainEvent(event: FeatureEvent) {
        when (event) {
            is FeatureEvent.ItemClicked -> onNavigate(event.itemId)
            is FeatureEvent.Retry -> loadData()
        }
    }

    private fun loadData() {
        scope.launch {
            _viewState.value = FeatureViewState.Loading
            getDataUseCase.execute()
                .onSuccess { _viewState.value = FeatureViewState.Success(it) }
                .onError { msg, _ -> _viewState.value = FeatureViewState.Error(msg) }
        }
    }

    @AssistedFactory
    interface Factory {
        operator fun invoke(
            componentContext: ComponentContext,
            onNavigate: (String) -> Unit,
        ): DefaultFeatureComponent
    }
}
```

### Component Rules
- **Single source of logic**
- Stores state (`Value<T>` from Decompose)
- Handles all events
- Executes use cases
- Manages lifecycle
- Navigation **ONLY** through Decompose:
  - `StackNavigation` / `childStack`
  - `SlotNavigation` / `childSlot`

### Component Dependencies
Allowed:
- Use cases
- Repositories (indirectly via use cases)
- Platform drivers (via DI)

Forbidden:
- Direct data source access
- UI imports (Compose)

## Use Case Layer

**File**: `<FeatureName><Action>UseCase.kt`

Project-defined `AppResult<T>` (NOT `kotlin.Result`) — carries explicit message + cause for UI surfacing:

```kotlin
// common/result/AppResult.kt
sealed class AppResult<out T> {
    data class Success<T>(val value: T) : AppResult<T>()
    data class Failure(val message: String, val cause: Throwable? = null) : AppResult<Nothing>()
}

inline fun <T> AppResult<T>.onSuccess(block: (T) -> Unit): AppResult<T> {
    if (this is AppResult.Success) block(value); return this
}
inline fun <T> AppResult<T>.onError(block: (String, Throwable?) -> Unit): AppResult<T> {
    if (this is AppResult.Failure) block(message, cause); return this
}
```

```kotlin
@Inject
class GetFeatureDataUseCase(
    private val repository: FeatureRepository
) {
    suspend fun execute(params: Params): AppResult<FeatureData> {
        return try {
            AppResult.Success(repository.getData(params.id))
        } catch (e: Exception) {
            AppResult.Failure(e.message ?: "Unknown error", e)
        }
    }
}
```

### Use Case Rules
- **One class per file**
- Returns only `AppResult<T>`
- Single `execute(params): AppResult<T>` function
- **NOT** an operator function
- All error handling happens here
- Dependencies:
  - Repository
  - TokenManager (if needed)
  - Platform drivers (if needed)
  - Other UseCases (rarely, for reuse)

## Repository Layer

**File**: `<FeatureName>Repository.kt`

```kotlin
@Inject
class FeatureRepository(
    private val localDataSource: FeatureLocalDataSource,
    private val remoteDataSource: FeatureRemoteDataSource
) {
    suspend fun getData(id: String): FeatureData {
        return try {
            remoteDataSource.fetch(id)
        } catch (e: Exception) {
            localDataSource.get(id) ?: throw e
        }
    }

    suspend fun saveData(data: FeatureData) {
        localDataSource.save(data)
        remoteDataSource.sync(data)
    }
}
```

### Repository Rules
- **Concrete class** (no interfaces needed for internal repos)
- Dependencies: only DataSources
- Returns clean data
- Coordinates local/remote sources

## DataSource Layer

**Files**:
- `<FeatureName>LocalDataSource.kt`
- `<FeatureName>RemoteDataSource.kt`

```kotlin
@Inject
class FeatureLocalDataSource(
    private val database: AppDatabase
) {
    suspend fun get(id: String): FeatureData? {
        return database.featureDao().getById(id)?.toDomain()
    }

    suspend fun save(data: FeatureData) {
        database.featureDao().insert(data.toEntity())
    }
}

@Inject
class FeatureRemoteDataSource(
    private val apiClient: ApiClient
) {
    suspend fun fetch(id: String): FeatureData {
        return apiClient.get("/features/$id").body<FeatureDto>().toDomain()
    }
}
```

### DataSource Rules
- Simple provider pattern
- Dependencies:
  - Local storage (Room, DataStore)
  - Platform APIs
  - Network client (Ktor)

**KMP HTTP client**: in `commonMain` DataSources use **Ktor `HttpClient`** — cross-platform. **OkHttp is JVM-only** — only acceptable in `jvmMain`/`androidMain` source sets. Don't reference `OkHttpClient` from `commonMain`.

## ViewState and Events

**File**: `<FeatureName>ViewState.kt`

Canonical 3-state template — fits **read-mostly** screens (lists, details, dashboards):

```kotlin
sealed class FeatureViewState {
    data object Loa
Files: 2
Size: 16.4 KB
Complexity: 20/100
Category: Design

Related in Design