Modo docs Help

Core Concepts

Modo is a state-based navigation library for Jetpack Compose. It represents the UI as a structure of Screens and ContainerScreens (which are implementations of Screen).

Navigation is a Graph

Each integration of Modo is a rooted tree (wiki) that can be displayed as follows:

modo graph
  • Each node is a Screen or ContainerScreen.

  • Leaf nodes are Screens.

  • Inner nodes are ContainerScreens. They can contain other Screens or ContainerScreens in their navigationState.

  • The root node is a RootScreen. You can have multiple roots in your app. See How to integrate Modo for details.

Screen

A Screen is the basic unit of the UI. It displays content defined in the overridden fun Content(modifier: Modifier).

// You need to use Parcelize plugin to generate Parcelable implementation for process death survavial @Parcelize class SampleScreen( // You can pass argiment as a constructor parameter private val screenIndex: Int, // You need to generate a unique screen key using special function override val screenKey: ScreenKey = generateScreenKey() ) : Screen { @Composable override fun Content(modifier: Modifier) { // Taking a nearest stack navigation container val stackNavigation = LocalStackNavigation.current SampleScreenContent( modifier = modifier, screenIndex = screenIndex, openNextScreen = { stackNavigation.forward(SampleScreen(screenIndex + 1)) }, ) } }

Screen Key

Each screen is identified by a ScreenKey - a unique key representing the screen. This key is extensively used in internal implementation. It must be unique for each screen, even after process death. To ensure this, use the built-in generateScreenKey function.

Arguments

To pass arguments to the screen, declare them in the Screen's constructor:

@Parcelize class SampleScreen( // You can pass argiment as a constructor parameter private val screenIndex: Int, override val screenKey: ScreenKey = generateScreenKey() ) : Screen {

Saving and Restoring

Each screen is Parcelable, which helps to save and restore it during lifecycle changes. Use the parcelable Gradle plugin and the @Parcelize annotation to generate the Parcelable implementation on the fly.

It's crucial to use built-in functions like rememberRootScreen to integrate Modo with your application. Read How to Integrate Modo into Your App for details.

ContainerScreen

ContainerScreens are types of screens that can contain other screens. They are fundamental building blocks for complex navigation structures. StackScreen and MultiScreen are built-in implementations of ContainerScreen.

diagram_2.png

Each ContainerScreen is defined by two typed parameters: State and Action.

@Stable abstract class ContainerScreen<State : NavigationState, Action : NavigationAction<State>>( private val navModel: NavModel<State, Action> ) : Screen, NavigationContainer<State, Action> by navModel

State

NavigationState - a class that can contain nested screens and other additional information. The state can be updated by calling dispatch(action).

@Parcelize data class SampleState( val screen1: Screen, val screen2: Screen, val screen3: Screen?, ) : NavigationState { // You need to return all nested screens to ensure correct functionality. override fun getChildScreens(): List = listOfNotNull(screen1, screen2, screen3) }

Read the State Update section for more details.

    Action

    NavigationAction - a marker interface to distinguish actions for this container on a specific State. You can also use ReducerAction to define actions with an in-place update function:

    fun interface SampleAction : ReducerAction<SampleState> { class Remove : SampleAction { override fun reduce(oldState: SampleState): SampleState = oldState.copy(screen3 = null) } class CreateScreen : SampleAction { override fun reduce(oldState: SampleState): SampleState = oldState.copy(screen3 = NestedScreen(canBeRemoved = true)) } }

      NavModel - a state storage responsible for state updates and triggering UI updates. Each ContainerScreen has a NavModel as a constructor parameter.

      Rendering Nested Screens

      To render nested screens inside a container screen, you must use the InternalContent function. This function provides all necessary integrations, such as:

      • Correct usage of rememberSaveable inside nested screens by using SaveableStateHolder.

      • Integration of ScreenModel, ensuring consistency for the same screen and clearing it when the Screen leaves the hierarchy.

      • Android integration, including Lifecycle and ViewModel support.

      The built-in StackScreen and MultiScreen use InternalContent under the hood to ensure correct nested screen functionality.

      State Update

      To update the state of a ContainerScreen, use dispatch(action: Action). There are two ways to define your action:

      ReducerAction allows defining the update function in-place.

      fun interface SampleAction : ReducerAction<SampleState> { class Remove : SampleAction { override fun reduce(oldState: SampleState): SampleState = oldState.copy(screen3 = null) } class CreateScreen : SampleAction { override fun reduce(oldState: SampleState): SampleState = oldState.copy(screen3 = NestedScreen(canBeRemoved = true)) } }

      Custom Reducer + Action

      You can provide a reducer in your ContainerScreen implementation.

      sealed interface SampleAction : NavigationAction<SampleState> { class Remove : SampleAction class CreateScreen : SampleAction } @Parcelize internal class RemovableItemContainerScreen( private val navModel: NavModel<RemovableItemContainerState, RemovableItemContainerAction> = NavModel( RemovableItemContainerState( NestedScreen(canBeRemoved = false), NestedScreen(canBeRemoved = false), NestedScreen(canBeRemoved = true), NestedScreen(canBeRemoved = false), ) ) ) : ContainerScreen<RemovableItemContainerState, RemovableItemContainerAction>(navModel) { override val reducer: NavigationReducer<RemovableItemContainerState, RemovableItemContainerAction> = NavigationReducer<RemovableItemContainerState, RemovableItemContainerAction> { action, state -> when (action) { is RemovableItemContainerAction.Remove -> { state.copy(screen3 = null) } is RemovableItemContainerAction.CreateScreen -> { state.copy(screen3 = NestedScreen(canBeRemoved = true)) } } } }

      Root Screen

      To integrate Modo into your application, use one of the built-in functions from the Modo file. It returns a RootScreen, which provides a SaveableStateHolder.

      Last modified: 12 June 2024