skills/decompose/SKILL.md
Decompose navigation and components - use for KMP component architecture, navigation, lifecycle, and state management
npx skillsauth add andvl1/claude-plugin decomposeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Component-based architecture with lifecycle management and navigation for KMP.
[versions]
decompose = "3.5.0"
essenty = "2.5.0"
[libraries]
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
decompose-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" }
essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" }
commonMain.dependencies {
implementation(libs.decompose)
implementation(libs.decompose.compose)
implementation(libs.essenty.lifecycle)
implementation(libs.kotlinx.serialization.json)
}
Business logic container with lifecycle. UI-agnostic.
// Interface (public API)
interface HomeComponent {
val state: Value<HomeState>
fun onItemClick(item: HomeItem)
fun onRefresh()
}
// Implementation
class DefaultHomeComponent(
componentContext: ComponentContext,
private val repository: HomeRepository,
private val onNavigateToDetails: (itemId: String) -> Unit
) : HomeComponent, ComponentContext by componentContext {
private val _state = MutableValue<HomeState>(HomeState.Loading)
override val state: Value<HomeState> = _state
private val scope = componentScope()
init {
loadData()
}
private fun loadData() {
scope.launch {
_state.value = HomeState.Loading
repository.getItems()
.onSuccess { items ->
_state.value = HomeState.Success(items)
}
.onError { message, _ ->
_state.value = HomeState.Error(message)
}
}
}
override fun onItemClick(item: HomeItem) {
onNavigateToDetails(item.id)
}
override fun onRefresh() {
loadData()
}
}
sealed class HomeState {
data object Loading : HomeState()
data class Success(val items: List<HomeItem>) : HomeState()
data class Error(val message: String) : HomeState()
}
Provides lifecycle, state preservation, and child management.
class MyComponent(
componentContext: ComponentContext
) : ComponentContext by componentContext {
// Access lifecycle
init {
lifecycle.subscribe(
onCreate = { println("Created") },
onStart = { println("Started") },
onResume = { println("Resumed") },
onPause = { println("Paused") },
onStop = { println("Stopped") },
onDestroy = { println("Destroyed") }
)
}
// Retain instances across config changes (Android)
private val viewModel = instanceKeeper.getOrCreate { MyViewModel() }
// Preserve state during process death
private var counter: Int by savedState("counter", 0)
// Create coroutine scope tied to lifecycle
private val scope = componentScope()
}
// Helper extension — place wherever your project keeps shared lifecycle utils.
// If you have a `core/` or `shared/core/` module, put it there so every feature can
// reuse it. If not, paste this snippet into your feature's impl module
// (e.g. `feature/<name>/impl/.../component/ComponentScope.kt`) — it has no Decompose-
// specific deps beyond `essenty`, so duplication is cheap.
import com.arkivanov.essenty.lifecycle.doOnDestroy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
fun ComponentContext.componentScope(): CoroutineScope {
val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
lifecycle.doOnDestroy { scope.cancel() }
return scope
}
Stack-based navigation like a navigation controller.
interface RootComponent {
val childStack: Value<ChildStack<Config, Child>>
sealed class Child {
data class Home(val component: HomeComponent) : Child()
data class Details(val component: DetailsComponent) : Child()
data class Settings(val component: SettingsComponent) : Child()
}
@Serializable
sealed class Config {
@Serializable data object Home : Config()
@Serializable data class Details(val itemId: String) : Config()
@Serializable data object Settings : Config()
}
}
class DefaultRootComponent(
componentContext: ComponentContext,
private val homeComponentFactory: HomeComponent.Factory,
private val detailsComponentFactory: DetailsComponent.Factory,
private val settingsComponentFactory: SettingsComponent.Factory
) : RootComponent, ComponentContext by componentContext {
private val navigation = StackNavigation<RootComponent.Config>()
override val childStack: Value<ChildStack<RootComponent.Config, RootComponent.Child>> =
childStack(
source = navigation,
serializer = RootComponent.Config.serializer(),
initialConfiguration = RootComponent.Config.Home,
handleBackButton = true, // Auto handle back
childFactory = ::createChild
)
private fun createChild(
config: RootComponent.Config,
context: ComponentContext
): RootComponent.Child = when (config) {
RootComponent.Config.Home -> RootComponent.Child.Home(
homeComponentFactory.create(
componentContext = context,
onNavigateToDetails = { itemId ->
navigation.push(RootComponent.Config.Details(itemId))
}
)
)
is RootComponent.Config.Details -> RootComponent.Child.Details(
detailsComponentFactory.create(
componentContext = context,
itemId = config.itemId,
onBack = { navigation.pop() }
)
)
RootComponent.Config.Settings -> RootComponent.Child.Settings(
settingsComponentFactory.create(context)
)
}
// Public navigation methods
fun navigateToSettings() {
navigation.push(RootComponent.Config.Settings)
}
}
Single optional active child.
interface HomeComponent {
val dialogSlot: Value<ChildSlot<DialogConfig, DialogChild>>
fun showConfirmDialog(itemId: String)
fun dismissDialog()
}
@Serializable
sealed class DialogConfig {
@Serializable data class Confirm(val itemId: String) : DialogConfig()
@Serializable data class Edit(val item: HomeItem) : DialogConfig()
}
sealed class DialogChild {
data class Confirm(val component: ConfirmDialogComponent) : DialogChild()
data class Edit(val component: EditDialogComponent) : DialogChild()
}
class DefaultHomeComponent(
componentContext: ComponentContext
) : HomeComponent, ComponentContext by componentContext {
private val dialogNavigation = SlotNavigation<DialogConfig>()
override val dialogSlot: Value<ChildSlot<DialogConfig, DialogChild>> =
childSlot(
source = dialogNavigation,
serializer = DialogConfig.serializer(),
childFactory = ::createDialog
)
private fun createDialog(
config: DialogConfig,
context: ComponentContext
): DialogChild = when (config) {
is DialogConfig.Confirm -> DialogChild.Confirm(
ConfirmDialogComponent(
context = context,
itemId = config.itemId,
onConfirm = { deleteItem(config.itemId); dismissDialog() },
onDismiss = ::dismissDialog
)
)
is DialogConfig.Edit -> DialogChild.Edit(
EditDialogComponent(context, config.item)
)
}
override fun showConfirmDialog(itemId: String) {
dialogNavigation.activate(DialogConfig.Confirm(itemId))
}
override fun dismissDialog() {
dialogNavigation.dismiss()
}
}
// Stack operations
navigation.push(Config.Details(itemId)) // Add to stack
navigation.pop() // Go back
navigation.pop { config -> config is Config.Home } // Pop to specific
navigation.replaceAll(Config.Home) // Clear and replace
navigation.replaceCurrent(Config.Other) // Replace top
navigation.navigate { stack -> stack + Config.X } // Atomic rebuild (single recomposition)
// Slot operations
dialogNavigation.activate(DialogConfig.Confirm(id)) // Show
dialogNavigation.dismiss() // Hide
navigate { ... } rebuilds whole stack atomically — use for deep links or any multi-step transition where intermediate states must not flicker through UI.
Bottom nav requires independent back stacks per tab — switching tabs must preserve each tab's history. Single root ChildStack cannot express this; use one StackNavigation + childStack(...) per tab, scoped under distinct childContext(key = ...) to keep their StateKeeper/InstanceKeeper/lifecycle isolated. Pick which tab is visible via SlotNavigation (or PagesNavigation if want swipe).
interface RootComponent {
val tabSlot: Value<ChildSlot<TabConfig, TabChild>>
fun selectTab(tab: TabConfig)
@Serializable
sealed class TabConfig {
@Serializable data object Home : TabConfig()
@Serializable data object Search : TabConfig()
@Serializable data object Profile : TabConfig()
}
sealed class TabChild {
data class Home(val component: HomeTabComponent) : TabChild()
data class Search(val component: SearchTabComponent) : TabChild()
data class Profile(val component: ProfileTabComponent) : TabChild()
}
}
class DefaultRootComponent(
componentContext: ComponentContext,
private val homeTabFactory: HomeTabComponent.Factory,
// ...
) : RootComponent, ComponentContext by componentContext {
private val tabNavigation = SlotNavigation<RootComponent.TabConfig>()
override val tabSlot: Value<ChildSlot<RootComponent.TabConfig, RootComponent.TabChild>> =
childSlot(
source = tabNavigation,
serializer = RootComponent.TabConfig.serializer(),
initialConfiguration = { RootComponent.TabConfig.Home },
// Keep all tabs alive so their back stacks survive tab switches.
// Without this, an inactive tab's components are destroyed and history is lost.
handleBackButton = false,
childFactory = ::createTab
)
private fun createTab(
config: RootComponent.TabConfig,
context: ComponentContext
): RootComponent.TabChild = when (config) {
RootComponent.TabConfig.Home -> RootComponent.TabChild.Home(
homeTabFactory.create(
// Distinct key per tab — gives each subtree its own
// StateKeeper/InstanceKeeper namespace under the same parent.
componentContext = context.childContext(key = "tab-home"),
)
)
// ... search, profile analogous
}
override fun selectTab(tab: RootComponent.TabConfig) {
// Re-tap on already-active tab → pop that tab's stack to root.
if (tabSlot.value.child?.configuration == tab) {
(tabSlot.value.child?.instance as? PoppableTab)?.popToRoot()
return
}
tabNavigation.activate(tab)
}
}
interface PoppableTab { fun popToRoot() }
// Per-tab component owns its own StackNavigation and ChildStack.
class DefaultHomeTabComponent(
componentContext: ComponentContext,
) : HomeTabComponent, PoppableTab, ComponentContext by componentContext {
private val navigation = StackNavigation<HomeConfig>()
override val childStack: Value<ChildStack<HomeConfig, HomeChild>> =
childStack(
source = navigation,
serializer = HomeConfig.serializer(),
initialConfiguration = HomeConfig.List,
// Important: only the active tab should consume back press;
// wire this through your platform back dispatcher.
handleBackButton = true,
childFactory = ::createChild,
key = "home-stack" // unique within this tab's context
)
override fun popToRoot() {
navigation.popWhile { it !is HomeConfig.List }
}
}
Why each piece:
childContext(key = "tab-home") — without unique keys, sibling tabs share StateKeeper slot names and one will overwrite the other on save/restore.SlotNavigation over StackNavigation for tab selector — tabs aren't a back stack; they're a flat switch. Using a stack here would push tabs onto each other.StackNavigation — back press inside Home tab pops Home's stack only, not the tab selection.@Composable
fun HomeScreen(component: HomeComponent) {
val state by component.state.subscribeAsState()
when (val currentState = state) {
is HomeState.Loading -> LoadingIndicator()
is HomeState.Error -> ErrorContent(
message = currentState.message,
onRetry = component::onRefresh
)
is HomeState.Success -> HomeContent(
items = currentState.items,
onItemClick = component::onItemClick
)
}
}
@Composable
fun RootContent(component: RootComponent) {
val childStack by component.childStack.subscribeAsState()
Children(
stack = childStack,
modifier = Modifier.fillMaxSize(),
animation = stackAnimation(fade() + slide())
) { child ->
when (val instance = child.instance) {
is RootComponent.Child.Home -> HomeScreen(instance.component)
is RootComponent.Child.Details -> DetailsScreen(instance.component)
is RootComponent.Child.Settings -> SettingsScreen(instance.component)
}
}
}
// Animation options
val animation = stackAnimation(
fade(), // Fade in/out
slide(), // Slide horizontal
scale(), // Scale
fade() + slide(), // Combined
slide(SlideAnimation.Top) // Slide from top
)
@Composable
fun HomeScreen(component: HomeComponent) {
val state by component.state.subscribeAsState()
val dialogSlot by component.dialogSlot.subscribeAsState()
Scaffold { paddingValues ->
// Main content
HomeContent(
modifier = Modifier.padding(paddingValues),
state = state,
onItemLongClick = { component.showConfirmDialog(it.id) }
)
// Dialog overlay
dialogSlot.child?.instance?.let { dialogChild ->
when (dialogChild) {
is DialogChild.Confirm -> ConfirmDialog(dialogChild.component)
is DialogChild.Edit -> EditDialog(dialogChild.component)
}
}
}
}
@Composable
private fun ConfirmDialog(component: ConfirmDialogComponent) {
AlertDialog(
onDismissRequest = component::onDismiss,
title = { Text("Delete Item?") },
text = { Text("This action cannot be undone.") },
confirmButton = {
TextButton(onClick = component::onConfirm) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = component::onDismiss) {
Text("Cancel")
}
}
)
}
Survives configuration changes on Android. Does NOT survive process death.
class MyComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
// Approach 1: Manual
private val viewModel = instanceKeeper.getOrCreate("viewModel") {
MyViewModel()
}
// Approach 2: Extension
private val viewModel by retainedInstance { MyViewModel() }
class MyViewModel : InstanceKeeper.Instance {
val state = MutableStateFlow<UiState>(UiState.Initial)
override fun onDestroy() {
// Cleanup when component truly destroyed
}
}
}
Survives process death. Data must be serializable.
class MyComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
// Approach 1: Delegate property
private var searchQuery: String by savedState("searchQuery", "")
private var selectedTab: Int by savedState("selectedTab", 0)
// Approach 2: Complex state
@Serializable
data class SavedState(
val query: String = "",
val filters: List<Filter> = emptyList(),
val scrollPosition: Int = 0
)
private var savedState: SavedState by savedState("state", SavedState())
// Approach 3: Manual
init {
stateKeeper.register("manualState") {
SavedState(query = currentQuery, filters = currentFilters)
}
stateKeeper.consume<SavedState>("manualState")?.let { restored ->
currentQuery = restored.query
currentFilters = restored.filters
}
}
}
feature/home/impl/src/commonMain/kotlin/
├── HomeComponent.kt # Interface
├── DefaultHomeComponent.kt # Implementation
├── HomeState.kt # State sealed class
├── di/
│ └── HomeModule.kt # Metro bindings
└── ui/
├── HomeScreen.kt # Compose UI
└── HomeContent.kt # UI components
// feature/home/api/src/commonMain/kotlin/HomeComponent.kt
interface HomeComponent {
val state: Value<HomeState>
val dialogSlot: Value<ChildSlot<*, DialogChild>>
fun onItemClick(item: HomeItem)
fun onRefresh()
fun showDeleteDialog(itemId: String)
fun dismissDialog()
interface Factory {
fun create(
componentContext: ComponentContext,
onNavigateToDetails: (String) -> Unit
): HomeComponent
}
}
// feature/home/impl/src/commonMain/kotlin/DefaultHomeComponent.kt
@Inject
class DefaultHomeComponent(
private val repository: HomeRepository,
@Assisted componentContext: ComponentContext,
@Assisted private val onNavigateToDetails: (String) -> Unit
) : HomeComponent, ComponentContext by componentContext {
// Implementation...
@AssistedFactory
interface Factory : HomeComponent.Factory {
override fun create(
componentContext: ComponentContext,
onNavigateToDetails: (String) -> Unit
): DefaultHomeComponent
}
}
Two delivery moments to handle: cold start (process not running, link arrives via Intent/launch options — root component constructed with link) and warm start (process alive, link arrives via onNewIntent on Android or notification delegate on iOS — must forward to existing root component, not rebuild it).
For a flat app, replaceAll(...) after construction works. But if landing screen needs a real back stack (tap notification → Details, press back → List), build initial stack via childStack(initialStack = ...) overload — avoids one-frame flicker of List → Details and gets correct back behavior on first frame.
class DefaultRootComponent(
componentContext: ComponentContext,
deepLink: DeepLink? = null
) : RootComponent, ComponentContext by componentContext {
private val navigation = StackNavigation<Config>()
override val childStack: Value<ChildStack<Config, Child>> =
childStack(
source = navigation,
serializer = Config.serializer(),
initialStack = { resolveInitialStack(deepLink) },
handleBackButton = true,
childFactory = ::createChild
)
private fun resolveInitialStack(deepLink: DeepLink?): List<Config> = when (deepLink) {
null -> listOf(Config.Home)
is DeepLink.ItemDetails -> listOf(Config.Home, Config.Details(deepLink.itemId))
is DeepLink.Settings -> listOf(Config.Home, Config.Settings)
}
// Warm-start entry: invoked from platform layer (see below).
fun onDeepLink(deepLink: DeepLink) {
navigation.navigate { stack ->
when (deepLink) {
is DeepLink.ItemDetails -> stack.dropLastWhile { it !is Config.Home } + Config.Details(deepLink.itemId)
is DeepLink.Settings -> stack.dropLastWhile { it !is Config.Home } + Config.Settings
}
}
}
}
sealed class DeepLink {
data class ItemDetails(val itemId: String) : DeepLink()
data object Settings : DeepLink()
}
navigate { stack -> ... } is the atomic operation: receives current stack, returns new stack — single recomposition, no transient state. Prefer it over chained pop()/push() for deep-link handling.
onNewIntentAndroid delivers warm-start links via onNewIntent on a singleTask/singleTop Activity. Hold the root component on the Activity and forward parsed link to it — do NOT recreate component or you lose all in-memory state.
// AndroidManifest.xml: <activity ... android:launchMode="singleTask">
class MainActivity : ComponentActivity() {
private lateinit var rootComponent: RootComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val deepLink = intent.data?.toString()?.let(::parseDeepLink)
rootComponent = createGraph<AndroidAppGraph>().rootComponentFactory.create(
componentContext = defaultComponentContext(),
deepLink = deepLink
)
setContent { AppTheme { RootContent(rootComponent) } }
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
intent.data?.toString()?.let(::parseDeepLink)?.let(rootComponent::onDeepLink)
}
}
UNUserNotificationCenterDelegate (or universal-link delegate) fires while app foregrounded. Bridge to Kotlin by exposing the root component (or a thin wrapper) from MainViewController and calling onDeepLink from Swift.
// shared
class IosAppEntry {
lateinit var rootComponent: RootComponent
private set
fun build(deepLink: DeepLink?): UIViewController { /* construct rootComponent, return ComposeUIViewController */ }
fun handleDeepLink(deepLink: DeepLink) = rootComponent.onDeepLink(deepLink)
}
// iOS app
func userNotificationCenter(_:didReceive response: UNNotificationResponse, withCompletionHandler ...) {
if let link = parseDeepLink(response.notification.request.content.userInfo) {
AppEntry.shared.handleDeepLink(deepLink: link)
}
}
With multi-tab structure, deep link to "item in Home tab" must (a) switch active tab to Home, (b) push Details onto Home's stack — flat replaceAll on root would clobber tab structure. Route the link through root, which dispatches to the right tab.
fun DefaultRootComponent.onDeepLink(deepLink: DeepLink) {
when (deepLink) {
is DeepLink.ItemDetails -> {
tabNavigation.activate(TabConfig.Home)
// After activation, the Home tab component exists in tabSlot —
// forward to it. The tab component owns its own StackNavigation.
(tabSlot.value.child?.instance as? RootComponent.TabChild.Home)
?.component
?.openDetails(deepLink.itemId)
}
is DeepLink.Profile -> tabNavigation.activate(TabConfig.Profile)
}
}
If the target tab isn't yet instantiated (cold start with a non-default initial tab), use initialConfiguration = { resolvedTab } on the tab childSlot and pre-build the tab's stack via the initialStack overload of its childStack(...).
Notification tap can launch process; if OS kills it before onCreate finishes, link is lost unless preserved. Two-layer safety: (1) Android delivers Intent via savedInstanceState-aware launch; (2) for in-flight links arriving during reconstruction, stash via StateKeeper:
class DefaultRootComponent(
componentContext: ComponentContext,
deepLink: DeepLink? = null
) : RootComponent, ComponentContext by componentContext {
// Persist any pending link that hasn't been consumed yet.
private var pendingLink: DeepLink? by savedState("pendingLink", deepLink)
init {
pendingLink?.let { link ->
// consume on first frame; clear so we don't re-apply on next restore
handleDeepLink(link)
pendingLink = null
}
}
}
Use this only for links whose target may not exist at construction time (e.g., requires async auth check). For synchronous routing, initialStack resolution is sufficient.
// Parse in platform code
fun parseDeepLink(uri: String): DeepLink? = when {
uri.contains("/item/") -> DeepLink.ItemDetails(uri.substringAfter("/item/"))
uri.contains("/settings") -> DeepLink.Settings
else -> null
}
class DetailsComponent(
componentContext: ComponentContext,
private val itemId: String,
private val onResult: (DetailsResult) -> Unit
) : ComponentContext by componentContext {
fun onSave(data: ItemData) {
// Save logic...
onResult(DetailsResult.Saved(data))
}
fun onDelete() {
// Delete logic...
onResult(DetailsResult.Deleted)
}
}
sealed class DetailsResult {
data class Saved(val data: ItemData) : DetailsResult()
data object Deleted : DetailsResult()
}
// In parent
private fun createDetailsChild(
config: Config.Details,
context: ComponentContext
): Child.Details = Child.Details(
DetailsComponent(
componentContext = context,
itemId = config.itemId,
onResult = { result ->
when (result) {
is DetailsResult.Saved -> refreshList()
DetailsResult.Deleted -> navigation.pop()
}
}
)
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val deepLink = intent.data?.toString()?.let(::parseDeepLink)
val graph = createGraph<AndroidAppGraph>()
val rootComponent = graph.rootComponentFactory.create(
componentContext = defaultComponentContext(),
deepLink = deepLink
)
setContent {
AppTheme {
RootContent(component = rootComponent)
}
}
}
}
fun MainViewController(deepLink: DeepLink? = null): UIViewController {
return ComposeUIViewController {
val rootComponent = remember {
val graph = createGraph<IosAppGraph>()
graph.rootComponentFactory.create(
componentContext = DefaultComponentContext(
lifecycle = ApplicationLifecycle()
),
deepLink = deepLink
)
}
AppTheme {
RootContent(component = rootComponent)
}
}
}
fun main() = application {
val lifecycle = LifecycleRegistry()
val graph = createGraph<DesktopAppGraph>()
val rootComponent = runOnUiThread {
graph.rootComponentFactory.create(
componentContext = DefaultComponentContext(lifecycle)
)
}
Window(onCloseRequest = ::exitApplication, title = "My Application") {
LifecycleController(lifecycle)
AppTheme {
RootContent(component = rootComponent)
}
}
}
Value<T> for observable state (not StateFlow)handleBackButton = true@Serializable for all Config classesclass HomeComponentTest {
@Test
fun `initial state is loading`() {
val component = DefaultHomeComponent(
componentContext = TestComponentContext(),
repository = FakeHomeRepository(),
onNavigateToDetails = {}
)
assertEquals(HomeState.Loading, component.state.value)
}
@Test
fun `loads items successfully`() = runTest {
val fakeRepo = FakeHomeRepository(items = listOf(testItem))
val component = DefaultHomeComponent(
componentContext = TestComponentContext(),
repository = fakeRepo,
onNavigateToDetails = {}
)
advanceUntilIdle()
val state = component.state.value
assertTrue(state is HomeState.Success)
assertEquals(1, (state as HomeState.Success).items.size)
}
}
// Test helper
class TestComponentContext : ComponentContext {
override val lifecycle = LifecycleRegistry()
override val stateKeeper = StateKeeperDispatcher()
override val instanceKeeper = InstanceKeeperDispatcher()
override val backHandler = BackDispatcher()
}
development
Effective Go patterns — idiomatic code, testing, benchmarks, project layout. Always use Go 1.21+ patterns.
development
Go microservices — gRPC, REST, cloud-native patterns, service discovery, circuit breakers, observability, health checks, graceful shutdown.
development
Go concurrency mastery — goroutines, channels, context, sync primitives, patterns, performance.
testing
Android WorkManager for guaranteed background execution - use for deferred tasks, periodic syncs, file uploads, notifications, and task chains. Covers CoroutineWorker, constraints, chaining, testing, and troubleshooting. Use when implementing background work that needs reliable execution across app restarts and doze mode.