How to Create Custom ComponentContext Implementations in Decompose
To create a custom ComponentContext in Decompose, define an interface extending GenericComponentContext<YourContext>, implement it using Kotlin delegation to a standard ComponentContext, and provide a ComponentContextFactory that wraps child contexts with your implementation.
When building complex applications with Decompose, the default ComponentContext may not provide all dependencies your components need, such as loggers or network clients. Learning how to create custom ComponentContext implementations in Decompose allows you to extend the framework's core context while maintaining full compatibility with navigation models like childStack and childNavigation. This guide walks through the architecture and implementation patterns found in the arkivanov/decompose repository.
Understanding the ComponentContext Architecture
Decompose's context system is built on a generic foundation that separates the essential lifecycle and state management capabilities from specific component implementations. The architecture centers on delegation and factory patterns that allow seamless extension without breaking existing navigation contracts.
Core Interfaces and Their Roles
The framework defines several key interfaces in decompose/src/commonMain/kotlin/com/arkivanov/decompose/:
-
GenericComponentContext<T>– The generic base interface defined inGenericComponentContext.ktthat extendsLifecycleOwner,StateKeeperOwner,InstanceKeeperOwner, andBackHandlerOwnerfrom Essenty. It also inheritsComponentContextFactoryOwner<T>, exposing thecomponentContextFactoryproperty required for creating child contexts. -
ComponentContext– The default concrete interface defined inComponentContext.ktthat extendsGenericComponentContext<ComponentContext>. This is what Decompose supplies by default when you create root components. -
ComponentContextFactory<T>– A functional interface defined inComponentContextFactory.ktresponsible for instantiating fresh context objects. Navigation models invoke this factory when spawning child contexts that are not attached to a parent. -
ComponentContextFactoryOwner<T>– Defined inComponentContextFactoryOwner.kt, this interface simply exposes thecomponentContextFactoryproperty that navigation utilities query when building component hierarchies.
Step-by-Step: Creating a Custom ComponentContext
To inject additional dependencies like loggers or API clients into your component tree, you create a custom context implementation following the delegation pattern documented in docs/component/custom-component-context.md.
Step 1: Define the Custom Interface
Create an interface that extends GenericComponentContext parameterized with your custom type. Add any additional properties your components require.
import com.arkivanov.decompose.GenericComponentContext
// Custom interface extending the generic base
interface AppComponentContext : GenericComponentContext<AppComponentContext> {
// Additional dependency available to all components using this context
val logger: Logger
}
Step 2: Implement with Kotlin Delegation
Create a concrete implementation class that receives a standard ComponentContext and delegates all Essenty owner interfaces to it. This preserves the lifecycle, state preservation, and back handling capabilities while adding your custom members.
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.backhandler.BackHandlerOwner
import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner
import com.arkivanov.essenty.lifecycle.LifecycleOwner
import com.arkivanov.essenty.statekeeper.StateKeeperOwner
class DefaultAppComponentContext(
private val componentContext: ComponentContext,
override val logger: Logger,
) : AppComponentContext,
// Delegate all Essenty owners to the supplied ComponentContext
LifecycleOwner by componentContext,
StateKeeperOwner by componentContext,
InstanceKeeperOwner by componentContext,
BackHandlerOwner by componentContext {
// Expose the factory for creating child contexts of the same custom type
override val componentContextFactory: ComponentContextFactory<AppComponentContext> =
ComponentContextFactory { lifecycle, stateKeeper, instanceKeeper, backHandler ->
// First create a plain ComponentContext using the original factory
val plainContext = componentContext.componentContextFactory(
lifecycle,
stateKeeper,
instanceKeeper,
backHandler
)
// Wrap it with our custom implementation, preserving the logger
DefaultAppComponentContext(plainContext, logger)
}
}
Step 3: Provide the ComponentContextFactory
The componentContextFactory property is crucial. When navigation models like childStack create child components, they invoke this factory. Your implementation must create a standard ComponentContext first, then wrap it with your custom class, ensuring that child components inherit the same additional capabilities (like the logger).
Using Your Custom Context in Components
When constructing components, request your custom context type instead of the default ComponentContext. All navigation utilities will automatically supply the correct type through the factory defined above.
class MyFeatureComponent(
private val context: AppComponentContext
) : Component {
init {
// Direct access to the custom logger
context.logger.d("MyFeatureComponent created")
}
fun onSomeAction() {
context.logger.d("Action triggered")
// Access lifecycle, stateKeeper, etc., via the delegated members
}
}
Because DefaultAppComponentContext implements AppComponentContext and properly exposes the componentContextFactory, functions like childStack, childNavigation, and childContext will automatically create child components with AppComponentContext instances that retain access to the logger.
Key Source Files in the Decompose Repository
Understanding the following files helps when debugging or extending the context system:
-
decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContext.kt– Defines the defaultComponentContextinterface extendingGenericComponentContext<ComponentContext>. -
decompose/src/commonMain/kotlin/com/arkivanov/decompose/GenericComponentContext.kt– Provides the generic base that bundles Essenty owners and the factory contract. -
decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContextFactory.kt– Specifies the functional interface for instantiating contexts. -
decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContextFactoryOwner.kt– Exposes the factory property required by navigation models. -
docs/component/custom-component-context.md– Official documentation with the pattern and sample code for creating custom contexts.
Summary
-
Extend
GenericComponentContext<T>to define a custom context interface that retains Decompose's core capabilities while adding your own dependencies. -
Use Kotlin delegation (
by) to delegateLifecycleOwner,StateKeeperOwner,InstanceKeeperOwner, andBackHandlerOwnerto a standardComponentContext, avoiding boilerplate. -
Implement
componentContextFactoryto wrap plain child contexts with your custom implementation, ensuring navigation utilities propagate your extended capabilities to child components. -
Reference the source in
decompose/src/commonMain/kotlin/com/arkivanov/decompose/to understand the factory pattern and generic architecture.
Frequently Asked Questions
Why would I need a custom ComponentContext instead of using constructor injection?
While constructor injection works for direct dependencies, a custom ComponentContext is essential when using Decompose's navigation utilities like childStack or childNavigation. These functions automatically create child components using the componentContextFactory, so wrapping dependencies in the context ensures they propagate down the component tree without manual passing through every constructor.
Does creating a custom ComponentContext break Decompose navigation?
No. As long as your custom context implements GenericComponentContext<YourType> and exposes a valid componentContextFactory, all navigation models work unchanged. The childStack, childNavigation, and childContext functions rely only on the ComponentContextFactoryOwner interface, which your implementation satisfies by delegating to the underlying context or providing your own factory.
How do I access the custom context in child components?
Child components receive your custom context automatically when you use the factory pattern described above. Simply declare your custom interface type (e.g., AppComponentContext) as the constructor parameter for child components. When childStack creates these children, it invokes your componentContextFactory, which wraps the child context with your custom implementation, preserving access to extended properties like loggers or API clients.
Can I add multiple custom dependencies to the same context?
Yes. You can add as many properties as needed to your custom interface (e.g., val logger: Logger, val apiClient: ApiClient, val config: AppConfig). Your implementation class receives all these dependencies via its constructor and exposes them through the interface. The componentContextFactory must then accept these dependencies and pass them down when wrapping child contexts, ensuring the entire component tree has access to the same shared services.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →