LazyChildItems in Decompose: Lazy Navigation for Dynamic Child Lists

LazyChildItems is a navigation model in the Decompose library that combines an observable Value<ChildItems<C, T>> with an ItemsNavigator<C> to create child components only when first accessed, making it ideal for lists, galleries, and dynamic UIs.

The arkivanov/decompose library provides powerful routing capabilities for Kotlin Multiplatform. When building screens with collections of child components—such as image galleries or data dashboards—you need a navigation pattern that balances state observation with memory efficiency. LazyChildItems solves this by deferring component instantiation until the moment a child is actually needed, while still preserving full lifecycle and state restoration capabilities.

What Is LazyChildItems?

At its core, LazyChildItems is an abstract class defined in /decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/items/LazyChildItems.kt that serves as both a Value holder and a navigation controller. It emits the current list of child items through Value<ChildItems<C, T>> while implementing ItemsNavigator<C> for navigation operations.

The defining characteristic is the required operator fun get(configuration: C): T method. This operator is invoked when you access a child by its configuration, and it must return the component instance—creating it lazily if it doesn't already exist. According to the source in LazyChildItems.kt lines 11-25, this is the only abstract member you must implement.

How LazyChildItems Works Under the Hood

The concrete implementation, DefaultLazyChildItems, resides in /decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/items/DefaultLazyChildItems.kt. This class delegates navigation commands to an internal ItemsController while managing the lazy instantiation logic.

When you call get(configuration), the implementation inserts the requested configuration with the lifecycle state CREATED before fetching the instance, as seen in lines 6-28 of DefaultLazyChildItems.kt. This ensures that the child component is properly registered in the navigation state before being returned, maintaining lifecycle consistency without eager instantiation of the entire list.

When to Use LazyChildItems

Choose LazyChildItems when your UI presents collections of child components where eager creation would waste memory or slow startup. Specific scenarios include:

  • Large or dynamic lists (e.g., a gallery of thumbnails): Only the visible or needed components are instantiated, reducing memory and start-up cost.
  • Complex lifecycle handling: The model tracks each child's lifecycle state (CREATED, STARTED, RESUMED) individually via ChildItems and ItemsNavigator, allowing you to start, stop, or destroy children independently.
  • State-preserving navigation: When using the childItems factory with a NavStateSaver (lines 30-45 of ChildItemsFactory.kt), the configuration list persists across process recreation while maintaining lazy instantiation.
  • Avoiding duplicate configurations: The model enforces uniqueness of configurations, preventing subtle bugs caused by duplicate keys (lines 14-18 of ChildItemsFactory.kt).
  • Main-thread safety: All API calls (including get) are intended for the main thread, guaranteeing thread-safe lifecycle transitions (lines 22-23 of LazyChildItems.kt).

Creating LazyChildItems with the childItems Factory

Rather than implementing the abstract class directly, you typically create instances using the childItems factory function defined in /decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/items/ChildItemsFactory.kt. This function constructs a DefaultLazyChildItems instance with full navigation and state-saving support.

The factory signature requires:

  • A NavigationSource<ItemsNavigation.Event<C>> to handle navigation commands
  • An initialItems lambda providing the starting Items<C> configuration
  • A childFactory lambda that creates the component instance given a configuration and ComponentContext

Optionally, you can provide a NavStateSaver<Items<C>> to enable automatic state preservation across configuration changes, as shown in lines 30-45 of ChildItemsFactory.kt.

Critical Usage Rules for LazyChildItems

The Decompose source code enforces several strict usage patterns to prevent lifecycle corruption and race conditions:

  1. Never call get recursively during navigation: Invoking get(configuration) while a navigation operation is in progress throws an IllegalStateException (documented in LazyChildItems.kt lines 16-20).
  2. Do not remove a child during its own instantiation: Attempting to destroy a component while get is creating it triggers the same exception.
  3. Maintain configuration uniqueness: Duplicate configuration values in the items list cause a runtime exception (enforced in ChildItemsFactory.kt lines 14-18).
  4. Restrict calls to the main thread: Both the childItems factory and LazyChildItems.get must be called on the main thread to ensure safe lifecycle transitions (noted in LazyChildItems.kt lines 22-23 and ChildItemsFactory.kt lines 19-20).

Practical Implementation Examples

The Decompose repository provides concrete implementations demonstrating both custom inheritance and factory-based creation.

Custom LazyChildItems Implementation

For scenarios where you need full control over component storage, inherit from LazyChildItems directly. The SimpleLazyChildItems sample in the repository shows a minimal implementation where all children are pre-instantiated but exposed through the lazy API:

@OptIn(ExperimentalDecomposeApi::class)
class SimpleLazyChildItems<C : Any, T : Any>(
    private val items: Map<C, T>,
) : LazyChildItems<C, T>(), ItemsNavigator<C> by ItemsNavigation() {

    private val _value = MutableValue(
        ChildItems(
            items = items.keys.toList(),
            activeItems = items.mapValues { (_, instance) -> instance to ActiveLifecycleState.CREATED }
        )
    )

    override val value: ChildItems<C, T> by _value::value
    override fun subscribe(observer: (ChildItems<C, T>) -> Unit) = _value.subscribe(observer)
    override fun get(configuration: C): T = items.getValue(configuration)
}

Source: SimpleLazyChildItems.kt

This pattern is useful when integrating with existing component caches or when testing navigation logic without full lazy instantiation.

For production UIs, use the childItems factory to create a navigable list. The sample gallery implementation demonstrates declaring the type and creating the instance:

interface GalleryComponent {
    @OptIn(ExperimentalDecomposeApi::class)
    val items: LazyChildItems<Image, ThumbnailComponent>
    fun onCloseClicked()
}

Source: GalleryComponent.kt

The factory invocation typically occurs in the component's implementation:

@OptIn(ExperimentalDecomposeApi::class)
fun ComponentContext.galleryChildItems(
    source: NavigationSource<ItemsNavigation.Event<Image>>,
    initialImages: List<Image>
): LazyChildItems<Image, ThumbnailComponent> =
    childItems(
        source = source,
        initialItems = { Items(items = initialImages) },
        childFactory = { image, ctx -> ThumbnailComponent(image, ctx) }
    )

Source: ChildItemsFactory.kt

This approach defers ThumbnailComponent creation until the UI actually requests a specific image, significantly improving startup performance for large galleries.

Summary

LazyChildItems provides a memory-efficient navigation pattern for Decompose applications that need to manage lists of child components. Key takeaways include:

  • LazyChildItems is an abstract class combining Value<ChildItems<C, T>> with ItemsNavigator<C> to create children on first access via the get(configuration) operator.
  • Use DefaultLazyChildItems (via the childItems factory) for production apps requiring navigation, state saving, and automatic lifecycle management.
  • Ideal for large lists, galleries, and dynamic dashboards where eager instantiation would waste memory or slow startup.
  • Always call get on the main thread, ensure configuration uniqueness, and never invoke get recursively during navigation operations.

Frequently Asked Questions

What is the difference between LazyChildItems and standard Child Items navigation?

LazyChildItems specifically defers component instantiation until the get(configuration) method is called, whereas standard child navigation might create components eagerly when the configuration list changes. This lazy behavior is essential for performance when dealing with large datasets or expensive component initialization, as it ensures only actively viewed children consume resources.

Can I use LazyChildItems without the childItems factory?

Yes, you can inherit directly from the abstract LazyChildItems class and implement the get operator yourself, as demonstrated in the SimpleLazyChildItems sample. However, for most production use cases, the childItems factory is recommended because it automatically handles navigation events, state serialization via NavStateSaver, and lifecycle state management through DefaultLazyChildItems.

How does LazyChildItems handle state preservation across process death?

When creating LazyChildItems via the childItems factory, you can optionally provide a NavStateSaver<Items<C>> parameter. This serializer persists the list of configurations across process recreation, while the lazy instantiation guarantee ensures that components are only recreated (via the childFactory lambda) when they are first accessed after restoration, not during the initial state recovery phase.

Is LazyChildItems thread-safe?

LazyChildItems is designed for single-threaded use on the main thread. The source code in LazyChildItems.kt explicitly requires that all API calls, including the get operator and factory initialization, occur on the main thread to ensure safe lifecycle transitions. While the internal implementation protects against concurrent modification during navigation, you must not call get from background threads or recursively during navigation operations.

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 →