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 in GenericComponentContext.kt that extends LifecycleOwner, StateKeeperOwner, InstanceKeeperOwner, and BackHandlerOwner from Essenty. It also inherits ComponentContextFactoryOwner<T>, exposing the componentContextFactory property required for creating child contexts.

  • ComponentContext – The default concrete interface defined in ComponentContext.kt that extends GenericComponentContext<ComponentContext>. This is what Decompose supplies by default when you create root components.

  • ComponentContextFactory<T> – A functional interface defined in ComponentContextFactory.kt responsible for instantiating fresh context objects. Navigation models invoke this factory when spawning child contexts that are not attached to a parent.

  • ComponentContextFactoryOwner<T> – Defined in ComponentContextFactoryOwner.kt, this interface simply exposes the componentContextFactory property 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:

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 delegate LifecycleOwner, StateKeeperOwner, InstanceKeeperOwner, and BackHandlerOwner to a standard ComponentContext, avoiding boilerplate.

  • Implement componentContextFactory to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →