| name | decompose |
| description | Use in the Blinkly Kotlin Multiplatform repository when adding, changing, or reviewing Decompose component contracts, default implementations, factories, navigation stacks, child outputs, tab navigation, or Compose bindings to Decompose `Value`, `ChildStack`, and component models under `shared/component/**` or `shared/compose/**`. |
Blinkly Decompose
Read AGENTS.md first for the project-level architecture.
Blinkly has baseline Decompose components across onboarding, home tabs,
root-pushed feature routes, Compose bindings, previews, and common component tests.
MainTabComponent remains the primary Store-backed tab reference, while
progress, reminders, and trainings show Store-backed tabs that emit
root-handled navigation outputs.
Use these local references first
Root app navigation:
shared/component/root/src/commonMain/kotlin/com/sedsoftware/blinkly/component/root/RootComponent.kt
shared/component/root/src/commonMain/kotlin/com/sedsoftware/blinkly/component/root/integration/RootComponentDefault.kt
shared/component/root/src/androidMain/kotlin/com/sedsoftware/blinkly/component/root/RootComponentFactory.kt
shared/component/root/src/iosMain/kotlin/com/sedsoftware/blinkly/component/root/RootComponentFactory.kt
Nested flow navigation:
shared/component/onboarding/src/commonMain/kotlin/com/sedsoftware/blinkly/component/onboarding/OnboardingComponent.kt
shared/component/onboarding/src/commonMain/kotlin/com/sedsoftware/blinkly/component/onboarding/integration/OnboardingComponentDefault.kt
Tabbed home shell and home tabs:
shared/component/home/src/commonMain/kotlin/com/sedsoftware/blinkly/component/home/integration/HomeScreenComponentDefault.kt
shared/component/main/src/commonMain/kotlin/com/sedsoftware/blinkly/component/main/MainTabComponent.kt
shared/component/main/src/commonMain/kotlin/com/sedsoftware/blinkly/component/main/integration/MainTabComponentDefault.kt
shared/component/main/src/commonMain/kotlin/com/sedsoftware/blinkly/component/main/integration/MainTabComponentPreview.kt
shared/component/main/src/commonMain/kotlin/com/sedsoftware/blinkly/component/main/integration/Mappers.kt
shared/component/progress/src/commonMain/kotlin/com/sedsoftware/blinkly/component/progress/ProgressTabComponent.kt
shared/component/progress/src/commonMain/kotlin/com/sedsoftware/blinkly/component/progress/integration/ProgressTabComponentDefault.kt
shared/component/progress/src/commonMain/kotlin/com/sedsoftware/blinkly/component/progress/integration/ProgressTabComponentPreview.kt
shared/component/reminders/src/commonMain/kotlin/com/sedsoftware/blinkly/component/reminders/RemindersTabComponent.kt
shared/component/reminders/src/commonMain/kotlin/com/sedsoftware/blinkly/component/reminders/integration/RemindersTabComponentDefault.kt
shared/component/reminders/src/commonMain/kotlin/com/sedsoftware/blinkly/component/reminders/integration/RemindersTabComponentPreview.kt
shared/component/trainings/src/commonMain/kotlin/com/sedsoftware/blinkly/component/trainings/TrainingsTabComponent.kt
shared/component/trainings/src/commonMain/kotlin/com/sedsoftware/blinkly/component/trainings/integration/TrainingsTabComponentDefault.kt
shared/component/trainings/src/commonMain/kotlin/com/sedsoftware/blinkly/component/trainings/integration/TrainingsTabComponentPreview.kt
Nested feature components:
shared/component/main/child/preferences/src/commonMain/kotlin/com/sedsoftware/blinkly/component/preferences/PreferencesComponent.kt
shared/component/main/child/preferences/src/commonMain/kotlin/com/sedsoftware/blinkly/component/preferences/integration/PreferencesComponentDefault.kt
shared/component/progress/child/achievements/src/commonMain/kotlin/com/sedsoftware/blinkly/component/achievements/AchievementsComponent.kt
shared/component/progress/child/achievements/src/commonMain/kotlin/com/sedsoftware/blinkly/component/achievements/integration/AchievementsComponentDefault.kt
shared/component/progress/child/garden/src/commonMain/kotlin/com/sedsoftware/blinkly/component/garden/GardenComponent.kt
shared/component/progress/child/garden/src/commonMain/kotlin/com/sedsoftware/blinkly/component/garden/integration/GardenComponentDefault.kt
shared/component/reminders/child/newreminder/src/commonMain/kotlin/com/sedsoftware/blinkly/component/newreminder/AddNewReminderComponent.kt
shared/component/reminders/child/newreminder/src/commonMain/kotlin/com/sedsoftware/blinkly/component/newreminder/integration/AddNewReminderComponentDefault.kt
shared/component/trainings/child/workout/src/commonMain/kotlin/com/sedsoftware/blinkly/component/workout/WorkoutComponent.kt
shared/component/trainings/child/workout/src/commonMain/kotlin/com/sedsoftware/blinkly/component/workout/integration/WorkoutComponentDefault.kt
Compose integration:
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/RootContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/home/HomeScreenContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/home/tabs/MainTabContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/home/tabs/ProgressTabContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/home/tabs/RemindersTabContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/home/tabs/TrainingsTabContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/preferences/PreferencesContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/achievements/AchievementsContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/garden/GardenContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/newreminder/AddNewReminderContent.kt
shared/compose/src/commonMain/kotlin/com/sedsoftware/blinkly/compose/ui/exercises/WorkoutContent.kt
Official docs:
Standard Blinkly component shape
For a normal feature component, create:
- interface in the module root package
integration/FeatureComponentDefault.kt
integration/FeatureComponentPreview.kt when Compose previews need stable fake data
- optional
Model in the interface
- optional
store/, domain/, and integration/Mappers.kt
Implementation rule:
class FeatureComponentDefault(...) : FeatureComponent, ComponentContext by componentContext
If the module only forwards output, keep it thin and skip MVIKotlin.
Default vs Preview rule:
*Default is the production implementation. It receives ComponentContext
and real dependencies, creates retained Stores, subscribes to labels, owns
lifecycle cleanup, and emits ComponentOutput.
*Preview is only for Compose previews. It implements the same public
component interface with MutableValue(Model(...)), static constructor
parameters, and no Store, ComponentContext, manager, or platform dependency.
MainTabComponentDefault and MainTabComponentPreview are the reference
pair. Do not wire preview implementations into parent factories.
Navigation rules
When the component owns navigation, use this pattern:
private val navigation = StackNavigation<Config>()
private val stack = childStack(...)
override val childStack: Value<ChildStack<*, Child>> = stack
- private
createChild(config, componentContext)
- private
onChildOutput(output)
- private sealed
@Serializable Config
Config rules:
- config contains only persistent arguments
- config must not contain dependencies
- prefer
data object or immutable data class
Keep navigation calls on the main thread.
Current Blinkly hierarchy:
RootComponentDefault owns the app-level stack and pushes Preferences,
Workout, Achievements, Garden, and AddNewReminder.
HomeScreenComponentDefault owns only the four-tab stack and forwards most
tab outputs upward to root.
OnboardingComponentDefault owns the onboarding step stack.
ProgressTabComponentDefault, RemindersTabComponentDefault, and
TrainingsTabComponentDefault do not own nested child stacks; they emit
ComponentOutput values that root interprets.
Output rules
Use ComponentOutput to communicate upward.
The child does not navigate directly through the parent.
Instead:
- child emits
ComponentOutput
- parent interprets it
- parent updates its own
StackNavigation
Reference output contract:
shared/domain/src/commonMain/kotlin/com/sedsoftware/blinkly/domain/model/ComponentOutput.kt
Constructor pattern for parent components
For parent components with children, follow the existing Blinkly pattern:
- make the primary constructor private
- pass child factory lambdas into the private constructor
- expose a secondary public constructor that wires real child implementations
This makes tests simpler because children can be swapped without reflection or a DI framework.
HomeScreenComponentDefault is the reference for passing real dependencies to
MainTabComponentDefault while still keeping the private constructor testable.
Lifecycle and scopes
Only create a custom component scope when the component itself runs background work.
If you create one, cancel it with lifecycle.doOnDestroy { scope.cancel() }.
For retained feature state, prefer instanceKeeper.getStore in the child component rather than hand-rolled state retention.
Compose integration rules
Compose stays outside components.
Use components as the only input to composables.
Preferred patterns:
val model by component.model.subscribeAsState() for component models
val stack by component.childStack.subscribeAsState() when observing Value manually
ChildStack(...) in root or parent content when rendering navigation
- render child tabs through the child component interface;
HomeScreenContent
maps HomeScreenComponent.Child.MainTab to MainTabContent(child.component)
- previews should pass a
*Preview component to the same production composable,
as MainTabContent does with MainTabComponentPreview
Do not create root components inside a composable unless there is no alternative.
Blinkly creates the root component in platform code before setContent; treat this as the host wiring pattern.
Root dependency composition rules
Root factories are the composition roots.
Use them to:
- create platform dispatchers
- build module factories from
shared/*/di
- assemble
DomainModule
- create
BeeperModule and pass BlinklyBeeper to workout execution
- instantiate
RootComponentDefault
Do not push DI container logic into feature components.
Keep feature components constructor-injected.
When adding a new screen or flow
- add a new component module in
shared/component/...
- define the interface and
*Default implementation
- add a child module if the screen is nested under another feature
- wire dependencies in the nearest parent component
- add navigation config and child mapping in the parent
- add Compose content in
shared/compose
- add common tests for navigation or behaviour
- update
AGENTS.md in the same change when the new screen or flow changes
module layout, component hierarchy, navigation ownership, dependency
direction, DI modules, external interfaces, or root factory wiring
For existing feature modules, preserve their component/store/Compose/test
shape and extend the nearest parent navigation only when the route changes.
Avoid these mistakes
- placing navigation logic in Compose
- putting dependencies into Decompose config classes
- skipping
@Serializable on config types
- creating a Store for a pure forwarding component
- creating the root
ComponentContext more than once per host lifecycle
- mixing Android-only types into common component APIs