Skip to content

Navigation 3 ​

Everything on this page lives in the optional navigation artifact. The package is jp.kaleidot725.pulse.navigation.

For how the pieces fit together, see the Navigation 3 guide. For using the core alone, see ViewModel.

rememberPulseViewModel ​

kotlin
@Composable
inline fun <reified ViewModel : PulseViewModel<*, *, *, *, *>> rememberPulseViewModel(
    key: String? = null,
    noinline factory: () -> ViewModel,
): ViewModel

Creates a ViewModel held in the ViewModelStore of the current ViewModelStoreOwner. A composition restart reuses the instance rather than rebuilding it.

Parameters ​

ParameterTypeDescription
keyString?Unique key within the owner. Defaults to the ViewModel's qualified class name
factory() -> ViewModelCalled once per owner to create the ViewModel

Lifecycle behavior ​

  • The ViewModel is created on first composition and reused for every later composition under the same owner
  • onSetup() runs once, the first time a PulseContent observes the ViewModel
  • Rebuilding the composition does not repeat onSetup()
  • The ViewModel scope is cancelled when the owner's ViewModelStore is cleared
  • State is kept in memory only; it is not restored after process death

Owner resolution ​

Internally this reads LocalViewModelStoreOwner.current. It keeps the instance in that owner's ViewModelStore, keyed by key. Whatever owner is in scope at the call site therefore decides how long the ViewModel lives. Anything that changes that owner changes the ViewModel's lifetime too.

Owner in scopeViewModel lives
The Compose Desktop WindowAs long as the screen; survives a composition restart
A Navigation 3 entry, via rememberPulseNavEntryDecorators() as NavDisplay's entryDecoratorsAs long as the route stays on the back stack; cancelled when the route is popped
One you provide with CompositionLocalProvider(LocalViewModelStoreOwner provides ...)As long as you keep that owner
None provided by the hostrememberPulseViewModel fails with a message: nothing owns a lifetime, so provide an owner or use mvi alone

Two consequences worth planning for ​

  • key is unique per owner, not globally. Two ViewModels of the same type under one owner collide, and the default key is the class name. Give them explicit keys, or put them under different owners
  • Nothing removes a ViewModel from its owner before the owner is cleared. Creating ViewModels under a long lived owner accumulates them for the life of that owner. Scope them to a narrower owner when a screen creates ViewModels it will not need again

TIP

Tests can exercise both sides of this by providing their own owner. Keep it across a composition rebuild to reproduce a composition restart. Clear it to reproduce the screen going away.

Example ​

kotlin
val viewModel = rememberPulseViewModel { CounterViewModel(CounterRepository()) }

// Two instances of the same ViewModel type need distinct keys
val left = rememberPulseViewModel(key = "left") { CounterViewModel(leftRepository) }
val right = rememberPulseViewModel(key = "right") { CounterViewModel(rightRepository) }

// Scoped to a Navigation 3 destination instead of the whole screen
NavDisplay(
    backStack = backStack,
    entryDecorators = rememberPulseNavEntryDecorators(),
    entryProvider = entryProvider {
        entry<Route.Counter> {
            val viewModel = rememberPulseViewModel { CounterViewModel(CounterRepository()) }
            // ...
        }
    },
)

rememberPulseContainer ​

kotlin
@Composable
inline fun <reified Container : PulseContainer<*, *>> rememberPulseContainer(
    key: String? = null,
    noinline factory: () -> Container,
): Container

Creates a Container that survives a composition restart. Its Unicast subscriptions stay alive. PulseContainer.close() is called when the owner's ViewModelStore is cleared.

Parameters ​

ParameterTypeDescription
keyString?Unique key within the owner. Defaults to the Container's qualified class name
factory() -> ContainerCalled once per owner to create the Container

Example ​

kotlin
val viewModel = rememberPulseViewModel { CounterViewModel(CounterRepository()) }
val container = rememberPulseContainer { CounterContainer(viewModels = listOf(viewModel)) }

rememberPulseNavEntryDecorators ​

kotlin
@Composable
fun <T : Any> rememberPulseNavEntryDecorators(): List<NavEntryDecorator<T>>

The NavEntryDecorator list to pass to NavDisplay. It is what scopes ViewModels to a back stack entry. It holds two decorators: the saveable state holder one, and the ViewModel one.

NavDisplay defaults entryDecorators to the saveable state holder alone. Passing the ViewModel decorator on its own would silently drop saveable state. This returns both.

kotlin
NavDisplay(
    backStack = backStack,
    entryDecorators = rememberPulseNavEntryDecorators(),
    entryProvider = entryProvider {
        entry<Route.Counter> {
            // Scoped to this entry: cancelled when the route is popped
            val viewModel = rememberPulseViewModel { CounterViewModel(CounterRepository()) }
            // ...
        }
    },
)

Released under the Apache 2.0 License.