npx claudepluginhub litun/decomposeclaudeplugin --plugin decomposeThis skill uses the workspace's default tool permissions.
You are helping implement navigation with Decompose. Choose the right model and 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 implement navigation with Decompose. Choose the right model and follow these patterns exactly.
| Scenario | Model |
|---|---|
| Screens pushed onto a stack (list → details, login → home) | ChildStack |
| One optional child at a time (dialog, modal, bottom sheet) | ChildSlot |
| Horizontal pager / tab-with-swipe (image gallery, onboarding) | ChildPages |
| Responsive master-detail layout (list + details side by side) | ChildPanels |
| Lazy list where each item is a live component | ChildItems |
| None of the above (carousel, custom state machine) | Generic Navigation |
Multiple navigation models in one component are fine — just use different key values.
@Serializable // kotlinx-serialization plugin required
private sealed interface Config {
@Serializable data object List : Config
@Serializable data class Details(val itemId: Long) : Config
}
Rules: @Serializable, immutable data class/data object, equals/hashCode via data class, small size (<500KB on Android), unique within ChildStack.
interface RootComponent {
val stack: Value<ChildStack<*, Child>>
fun onBackClicked(toIndex: Int)
sealed class Child {
class ListChild(val component: ListComponent) : Child()
class DetailsChild(val component: DetailsComponent) : Child()
}
}
class DefaultRootComponent(componentContext: ComponentContext) : RootComponent, ComponentContext by componentContext {
private val nav = StackNavigation<Config>()
override val stack: Value<ChildStack<*, RootComponent.Child>> =
childStack(
source = nav,
serializer = Config.serializer(),
initialConfiguration = Config.List,
handleBackButton = true,
childFactory = ::createChild,
)
private fun createChild(config: Config, ctx: ComponentContext): RootComponent.Child =
when (config) {
is Config.List -> ListChild(DefaultListComponent(ctx, onItemSelected = { nav.pushNew(Config.Details(it)) }))
is Config.Details -> DetailsChild(DefaultDetailsComponent(ctx, config.itemId, onFinished = nav::pop))
}
override fun onBackClicked(toIndex: Int) = nav.popTo(index = toIndex)
@Serializable private sealed interface Config {
@Serializable data object List : Config
@Serializable data class Details(val itemId: Long) : Config
}
}
Key operations: push, pushNew (guard double-tap), pushToFront, pop, popTo(index), popWhile { }, replaceCurrent, replaceAll, bringToFront (tabs only).
Result delivery:
nav.pop { // onComplete runs after navigation
(stack.active.instance as? ListChild)?.component?.onItemDeleted(itemId)
}
class DefaultCounterComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val dialogNav = SlotNavigation<DialogConfig>()
val dialogSlot: Value<ChildSlot<*, DialogComponent>> =
childSlot(
source = dialogNav,
serializer = null,
handleBackButton = true,
childFactory = { config, _ -> DefaultDialogComponent(config.value, dialogNav::dismiss) },
)
fun onInfoClicked() = dialogNav.activate(DialogConfig(value = 42))
@Serializable private data class DialogConfig(val value: Int)
}
Operations: activate(config), dismiss().
In Compose: slot.child?.instance?.also { dialog -> ... }
class DefaultGalleryComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val nav = PagesNavigation<ImageId>()
val pages: Value<ChildPages<*, ImageComponent>> =
childPages(
source = nav,
serializer = ImageId.serializer(),
initialPages = { Pages(items = ImageId.entries, selectedIndex = 0) },
childFactory = { id, ctx -> DefaultImageComponent(ctx, id) },
)
fun selectPage(index: Int) = nav.select(index = index)
}
Operations: select(index), selectNext(), selectPrev(), selectFirst(), selectLast(), setItems(items, selectedIndex).
class DefaultMultiPaneComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val nav = PanelsNavigation<Unit, DetailsConfig, ExtraConfig>()
val panels = childPanels(
source = nav,
initialPanels = { Panels(main = Unit) },
serializers = Triple(null, DetailsConfig.serializer(), ExtraConfig.serializer()),
handleBackButton = true,
mainFactory = { _, ctx -> MainChild(DefaultListComponent(ctx, onItemSelected = { id -> nav.navigate { it.copy(details = DetailsConfig(id)) } })) },
detailsFactory = { config, ctx -> DetailsChild(DefaultDetailsComponent(ctx, config.itemId, onFinished = { nav.navigate { it.copy(details = null) } })) },
extraFactory = { config, ctx -> ExtraChild(DefaultExtraComponent(ctx, config)) },
)
fun setMode(mode: ChildPanelsMode) = nav.navigate { it.copy(mode = mode) }
}
Modes: SINGLE (phone), DUAL (tablet), TRIPLE (large tablet/desktop). Drive from Compose via BoxWithConstraints + LaunchedEffect.
private val mainStack = childStack(source = mainNav, key = "MainStack", ...)
private val sideStack = childStack(source = sideNav, key = "SideStack", ...)
override val stack = childStack(
source = nav,
serializer = Config.serializer(),
initialStack = { buildInitialStack(deepLinkUrl) },
childFactory = ::createChild,
)
DecomposeSettings.duplicateConfigurationsEnabled = true to allow)bringToFront for tabs, not push — push creates duplicatespushNew not push to guard against double-tapsserializer = null if you intentionally don't want state saved (e.g., transient dialogs)key — default "default" collides