From decompose
Guides creation of Decompose components in Kotlin using interface + Default impl pattern, Value/MutableValue state, lifecycle handling, state preservation across config changes, and back button support.
How this skill is triggered — by the user, by Claude, or both
Slash command
/decompose:decompose-componentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are helping write a Decompose component. Follow these patterns exactly.
You are helping write a Decompose component. Follow these patterns exactly.
Always use interface + DefaultXxxComponent. Never extend a library base class.
interface CounterComponent {
val model: Value<Model>
fun onIncrementClicked()
data class Model(val count: Int = 0)
}
class DefaultCounterComponent(
componentContext: ComponentContext,
private val onFinished: () -> Unit, // callbacks to parent go via constructor
) : CounterComponent, ComponentContext by componentContext { // <-- delegation, not inheritance
private val _model = MutableValue(CounterComponent.Model())
override val model: Value<CounterComponent.Model> = _model
override fun onIncrementClicked() {
_model.update { it.copy(count = it.count + 1) }
}
}
Rules:
ComponentContext by componentContext — always delegate, never extendcomponentContext: ComponentContext as first parameterValue<T> (immutable), hold MutableValue<T> privatelyMutableValue.update { } for state mutations — call only on the main threadValue<T> is Decompose's multiplatform observable. Prefer it for cross-platform components.
// Good — works on all platforms, observable in Compose/SwiftUI/React
val state: Value<State> = _state
// Also acceptable if you're coroutines-only (Android/JVM):
val state: StateFlow<State>
Value is NOT a coroutine — no collect, use subscribe/subscribeAsState() in Compose.
Components get lifecycle automatically. Subscribe only in init or lifecycle callbacks.
class DefaultSomeComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
init {
lifecycle.doOnStart { /* start polling */ }
lifecycle.doOnStop { /* stop polling */ }
lifecycle.doOnDestroy { /* cleanup */ }
}
}
Lifecycle states: INITIALIZED → CREATED → STARTED → RESUMED → STOPPED → DESTROYED
RESUMEDCREATED (still alive, stopped)@Serializable
private data class State(val query: String = "", val selectedId: Long? = null)
class DefaultSearchComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private var state: State by saveable(serializer = State.serializer(), init = ::State)
}
Manual version:
private var state = stateKeeper.consume("STATE", State.serializer()) ?: State()
init {
stateKeeper.register("STATE", State.serializer()) { state }
}
Rules: @Serializable required, keep state small (<500KB on Android), consume() only once.
class DefaultTimerComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val timer = retainedInstance { Timer() }
private class Timer : InstanceKeeper.Instance {
override fun onDestroy() {}
}
}
Rules: NOT inner class, no Activity/Context/View references, implement onDestroy().
class DefaultComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val logic by saveable(serializer = Logic.State.serializer(), state = { it.state }) { savedState ->
retainedInstance { Logic(savedState) }
}
private class Logic(savedState: Logic.State?) : InstanceKeeper.Instance {
var state = savedState ?: Logic.State()
private set
@Serializable data class State(val items: List<String> = emptyList())
override fun onDestroy() {}
}
}
class DefaultEditorComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val backCallback = BackCallback(isEnabled = false) {
showDiscardDialog()
}
init { backHandler.register(backCallback) }
fun onFormChanged() { backCallback.isEnabled = true }
}
Priority: last-registered wins. Use priority = Int.MAX_VALUE to always intercept first.
class PreviewCounterComponent : CounterComponent {
override val model: Value<Model> = MutableValue(Model(count = 42))
override fun onIncrementClicked() {}
}
internal val PreviewContext: ComponentContext = DefaultComponentContext(LifecycleRegistry())
@Composable functiondefaultComponentContext() must be called only once per Activity/Fragment lifetimeretainedComponent() has the same restriction — call only once in onCreateMutableValue: subscribe and update only on the main thread — off-thread calls cause race conditions| Need | API |
|---|---|
| Component context delegation | class Foo(ctx: ComponentContext) : ComponentContext by ctx |
| Observable state | MutableValue<T> / Value<T> |
| Update state | _state.update { it.copy(...) } |
| Survive config change | retainedInstance { } |
| Survive process death | saveable(serializer = ...) or stateKeeper |
| Lifecycle events | lifecycle.doOnStart/Stop/Destroy { } |
| Custom back handling | backHandler.register(BackCallback { }) |
| Auto back in navigation | handleBackButton = true in childStack()/childSlot() |
npx claudepluginhub litun/decomposeclaudeplugin --plugin decomposeIntegrates Decompose navigation with Jetpack Compose and Multiplatform Compose UI using subscribeAsState, Children composables, stack animations, predictive back gestures, ChildSlots, and back handling.
Guides building native Android UIs with Jetpack Compose, including state management via remember/mutableStateOf, state hoisting, and ViewModel integration.
Provides expertise in Jetpack Compose and Compose Multiplatform for UI development across Android, Desktop, iOS, Web. Covers APIs, navigation, Paging 3, Android TV, design systems, and PR reviews.