npx claudepluginhub litun/decomposeclaudeplugin --plugin decomposeThis skill uses the workspace's default tool permissions.
You are helping wire Decompose components to Compose UI. Follow these patterns exactly.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Share bugs, ideas, or general feedback.
You are helping wire Decompose components to Compose UI. Follow these patterns exactly.
Convert Value<T> to Compose State<T> with subscribeAsState().
import com.arkivanov.decompose.extensions.compose.subscribeAsState
@Composable
fun CounterContent(component: CounterComponent, modifier: Modifier = Modifier) {
val model by component.model.subscribeAsState() // auto-subscribes and unsubscribes
Column(modifier = modifier) {
Text(text = model.count.toString())
Button(onClick = component::onIncrementClicked) { Text("Increment") }
}
}
Rules: Always use by delegation. Never subscribe manually. Expose a Model data class, not raw mutable state.
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.scale
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
@Composable
fun RootContent(component: RootComponent, modifier: Modifier = Modifier) {
Children(
stack = component.stack,
modifier = modifier,
animation = stackAnimation(fade() + scale()),
) {
when (val child = it.instance) {
is RootComponent.Child.ListChild -> ListContent(child.component)
is RootComponent.Child.DetailsChild -> DetailsContent(child.component)
}
}
}
Animations (combinable with +): fade(), scale(), slide(), or fully custom stackAnimation { ... }.
@OptIn(ExperimentalDecomposeApi::class)
@Composable
fun RootContent(component: RootComponent, modifier: Modifier = Modifier) {
Children(
stack = component.stack,
modifier = modifier,
animation = predictiveBackAnimation(
backHandler = component.backHandler,
fallbackAnimation = stackAnimation(fade() + scale()),
onBack = component::onBackClicked,
),
) { /* content */ }
}
Interface must extend BackHandlerOwner: interface RootComponent : BackHandlerOwner { ... }
val dialogSlot by component.dialogSlot.subscribeAsState()
dialogSlot.child?.instance?.also { dialog ->
AlertDialog(
onDismissRequest = dialog::onDismissClicked,
text = { Text(dialog.message) },
confirmButton = { Button(onClick = dialog::onDismissClicked) { Text("OK") } },
)
}
import com.arkivanov.decompose.extensions.compose.pages.ChildPages
import com.arkivanov.decompose.extensions.compose.pages.PagesScrollAnimation
ChildPages(
pages = component.pages,
onPageSelected = component::selectPage,
modifier = modifier,
scrollAnimation = PagesScrollAnimation.Default,
) { _, page ->
ImageContent(component = page, modifier = Modifier.fillMaxSize())
}
BoxWithConstraints(modifier = modifier) {
val mode = when {
maxWidth >= 1200.dp -> ChildPanelsMode.TRIPLE
maxWidth >= 800.dp -> ChildPanelsMode.DUAL
else -> ChildPanelsMode.SINGLE
}
LaunchedEffect(mode) {
component.setMode(mode)
}
ChildPanels(
panels = panels,
mainChild = { ArticleListContent(it.instance) },
detailsChild = { ArticleDetailsContent(it.instance) },
layout = HorizontalChildPanelsLayout(dualWeights = Pair(0.4f, 0.6f), tripleWeights = Triple(0.3f, 0.4f, 0.3f)),
)
}
@Composable
fun TabsContent(component: TabsComponent, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Children(stack = component.stack, modifier = Modifier.weight(1f)) {
when (val child = it.instance) {
is TabsComponent.Child.HomeChild -> HomeContent(child.component)
is TabsComponent.Child.ProfileChild -> ProfileContent(child.component)
}
}
val stack by component.stack.subscribeAsState()
val active = stack.active.instance
NavigationBar {
NavigationBarItem(selected = active is TabsComponent.Child.HomeChild, onClick = component::onHomeTabClicked, icon = { Icon(Icons.Default.Home, null) }, label = { Text("Home") })
NavigationBarItem(selected = active is TabsComponent.Child.ProfileChild, onClick = component::onProfileTabClicked, icon = { Icon(Icons.Default.Person, null) }, label = { Text("Profile") })
}
}
}
Component uses bringToFront for tab switching: fun onHomeTabClicked() = nav.bringToFront(Config.Home)
application {
val windowState = rememberWindowState()
LifecycleController(lifecycle, windowState) // required on Desktop
Window(onCloseRequest = ::exitApplication, state = windowState) { RootContent(root) }
}
class PreviewCounterComponent : CounterComponent {
override val model: Value<CounterComponent.Model> = MutableValue(CounterComponent.Model(count = 42))
override val dialogSlot: Value<ChildSlot<*, DialogComponent>> = MutableValue(ChildSlot())
override fun onIncrementClicked() {}
override fun onInfoClicked() {}
}
@Preview @Composable
fun CounterContentPreview() { CounterContent(PreviewCounterComponent()) }
For ComponentContext in previews: val PreviewContext = DefaultComponentContext(LifecycleRegistry())
DisposableEffect(component.backHandler, hasChanges) {
val callback = BackCallback(isEnabled = hasChanges) { component.onBackClicked() }
component.backHandler.register(callback)
onDispose { component.backHandler.unregister(callback) }
}
Avoid Jetpack's BackHandler {} composable — it bypasses the component hierarchy.
@Composable — may run on background threadsubscribeAsState() handles subscription lifecycle — don't call subscribe()/unsubscribe() manuallyChildren() manages SaveableStateHolder — don't add rememberSaveableStateHolder yourselfLifecycleController — without it components won't receive lifecycle eventsbringToFront not push for tabscommonMain.dependencies {
implementation("com.arkivanov.decompose:decompose:$decomposeVersion")
implementation("com.arkivanov.decompose:extensions-compose:$decomposeVersion")
}