一键导入
compose-arch
Compose Multiplatform Architecture Framework - strict Screen/View/Component layering, use cases, repositories, and feature slice patterns
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Compose Multiplatform Architecture Framework - strict Screen/View/Component layering, use cases, repositories, and feature slice patterns
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Go concurrency mastery — goroutines, channels, context, sync primitives, patterns, performance.
Go microservices — gRPC, REST, cloud-native patterns, service discovery, circuit breakers, observability, health checks, graceful shutdown.
Effective Go patterns — idiomatic code, testing, benchmarks, project layout. Always use Go 1.21+ patterns.
Decompose navigation and components - use for KMP component architecture, navigation, lifecycle, and state management
Procedural KMP feature generation workflow — step-by-step creation of feature slices with typed errors, compose-arch compliance, and build verification. Use when scaffolding a new feature module in a KMP/Compose Multiplatform project.
Kotlin + Spring Boot 4.0.x patterns — use for backend services, REST APIs, DI, controllers, services. Always use these versions verbatim; do not downgrade to Spring Boot 3.x or Kotlin 2.1.x even if your training data suggests older releases.
| name | compose-arch |
| description | Compose Multiplatform Architecture Framework - strict Screen/View/Component layering, use cases, repositories, and feature slice patterns |
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)| 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 |
File: <FeatureName>Screen.kt
@Composable
fun FeatureScreen(component: FeatureComponent) {
val viewState by component.viewState.subscribeAsState()
FeatureView(viewState, component::obtainEvent)
}
remember callsFile: <FeatureName>View.kt
@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) }
)
}
}
}
AppTheme.colors, AppTheme.typographycommon/ui/ if used in 5+ placesFile: <FeatureName>Component.kt
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
}
}
Value<T> from Decompose)StackNavigation / childStackSlotNavigation / childSlotAllowed:
Forbidden:
File: <FeatureName><Action>UseCase.kt
Project-defined AppResult<T> (NOT kotlin.Result) — carries explicit message + cause for UI surfacing:
// 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
}
@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)
}
}
}
AppResult<T>execute(params): AppResult<T> functionFile: <FeatureName>Repository.kt
@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)
}
}
Files:
<FeatureName>LocalDataSource.kt<FeatureName>RemoteDataSource.kt@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()
}
}
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.
File: <FeatureName>ViewState.kt
Canonical 3-state template — fits read-mostly screens (lists, details, dashboards):
sealed class FeatureViewState {
data object Loading : FeatureViewState()
data class Success(val data: List<FeatureItem>) : FeatureViewState()
data class Error(val message: String) : FeatureViewState()
}
3-state template wipes draft input on save failure — unacceptable for forms. Two options:
Option A: extend Success with inlineError:
data class Editing(
val draft: FeatureDraft,
val saving: Boolean = false,
val inlineError: String? = null,
) : FeatureViewState()
Option B: separate Editing state alongside Loading/Success/Error:
sealed class FormViewState {
data object Loading : FormViewState()
data class Editing(
val draft: FeatureDraft,
val saving: Boolean = false,
val generalError: String? = null,
val fieldErrors: Map<String, String> = emptyMap(),
) : FormViewState()
data class Saved(val id: String) : FormViewState()
data class Error(val message: String) : FormViewState() // fatal load errors only
}
Rule: save failure → keep draft, set inlineError/generalError. Never drop user input.
File: <FeatureName>ViewEvent.kt
sealed class FeatureEvent {
data class ItemClicked(val itemId: String) : FeatureEvent()
data object Retry : FeatureEvent()
data object BackPressed : FeatureEvent()
}
One class per file:
NO god files - split immediately if file grows beyond responsibility.
feature/<featureName>/
├── api/ # Public interfaces
│ └── src/commonMain/kotlin/
│ ├── <Name>Component.kt # Interface only
│ ├── <Name>Models.kt # Domain models
│ └── <Name>Repository.kt # Interface (if public)
│
└── impl/ # Implementation
└── src/commonMain/kotlin/
├── screen/
│ └── <Name>Screen.kt
├── view/
│ ├── <Name>View.kt
│ ├── <Name>ViewState.kt
│ └── <Name>ViewEvent.kt
├── component/
│ └── Default<Name>Component.kt
├── domain/
│ ├── usecase/
│ │ ├── Get<Name>UseCase.kt
│ │ └── Update<Name>UseCase.kt
│ └── repository/
│ └── <Name>Repository.kt
├── data/
│ └── datasource/
│ ├── <Name>LocalDataSource.kt
│ └── <Name>RemoteDataSource.kt
└── di/
└── <Name>Module.kt
File: <FeatureName>Module.kt
@BindingContainer
class FeatureModule {
@Provides
fun provideFeatureRepository(
localDataSource: FeatureLocalDataSource,
remoteDataSource: FeatureRemoteDataSource
): FeatureRepository = FeatureRepository(localDataSource, remoteDataSource)
}
| Rule | Details |
|---|---|
| Serialization | Only Kotlinx Serialization |
| JSON | Single instance via DI |
| Repository return | Clean domain data |
| UseCase return | Always AppResult<T> (or AppResult<Flow<T>>) |
| Error handling | All in UseCase (where Result is created) |
| State | Never use remember in View - state from Component |
Extract to common/ui/<ComponentName>.kt when:
// common/ui/LoadingButton.kt
@Composable
fun LoadingButton(
text: String,
loading: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
enabled = !loading,
modifier = modifier
) {
if (loading) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp
)
} else {
Text(text)
}
}
}
Before completing a feature, verify:
| Anti-Pattern | Correct Pattern |
|---|---|
| Logic in Screen | Move to Component |
| remember in View | State from Component |
| Direct API calls in Component | Use UseCase |
| UseCase calling DataSource | Use Repository |
| God file with multiple classes | Split to separate files |
| Deep nesting (4+ levels) | Extract sub-components |
| Hardcoded colors/dimensions | Use theme |