Modo Navigation Library

repository·dev·Indexed 43 days ago

https://github.com/ikarenkov/modo

A state-based navigation library for Jetpack Compose that models navigation as a rooted tree graph. It uses a Unidirectional Data Flow (UDF) approach where UI is a direct reflection of NavigationState, utilizing RootScreen, ContainerScreen, and Screen nodes. Features include Android integration for ViewModel and Lifecycle via ModoScreenAndroidAdapter, stack-based navigation actions, and state management through NavigationReducer and NavModel.

Tokens
13.6K
Snippets
38
Records
64
Agent score
53%

What's inside Modo

  1. What is Modo and how does it work?

    dev
    Modo is a state-based navigation library for Jetpack Compose. Unlike traditional imperative navigation, Modo treats navigation as a state. The UI is defined by a NavigationState, which is composed of Screen and ContainerScreen objects. Because navigation is just state, you can modify the NavigationState from any part of your application to change the UI.
  2. Use Screen Effects for lifecycle-aware side effects

    dev

    Modo provides side effects that are tied to the Screen lifecycle rather than the standard Jetpack Compose composition lifecycle. These effects are executed once per Screen instance when the screen enters the composition and are disposed of when the screen is removed from the navigation graph.

    Available effects:

    • LaunchedScreenEffect: Executes a suspend CoroutineScope.() -> Unit block when the screen is created. The coroutine scope is automatically cancelled when the screen is removed.
    • DisposableScreenEffect: Executes a DisposableEffectScope.() -> DisposableEffectResult block when the screen is created. It calls dispose() on the returned result when the screen is removed.
    • OnScreenRemoved: Executes a block when the screen is removed from the navigation graph (similar to the disposal phase of DisposableScreenEffect).
    • LifecycleScreenEffect: A shortcut for subscribing to a screen's lifecycle, implemented using DisposableScreenEffect.
    // Example of DisposableScreenEffect for analytics
    DisposableScreenEffect {
        logcat { "Analytics: screen created. Counter: $counter." }
        onDispose {
            logcat { "Analytics: screen destroyed. Counter: $counter." }
        }
    }
  3. Understand Modo's navigation graph model

    dev

    Modo treats navigation as a rooted tree structure. This graph-based approach allows you to organize your UI into a hierarchy of screens and containers:

    • RootScreen: The root node of the tree. An application can have multiple roots.
    • ContainerScreen: Inner nodes in the graph. They act as parents that can contain and render other Screens or ContainerScreens within their NavigationState.
    • Screen: Leaf nodes in the graph. These represent individual UI units that do not contain other screens.

    By structuring navigation as a graph, you can compose complex UI hierarchies where state changes in a parent container drive the visibility and content of its children.

  4. Define a Screen in Modo

    dev

    A Screen is the fundamental unit of the UI in Modo. To create one, extend the Screen class and override the Content(modifier: Modifier) function to define your Jetpack Compose UI.

    To pass data to a screen, declare the required data as parameters in the Screen constructor. Each screen must have a unique ScreenKey to ensure it can be correctly identified and restored after process death; use the generateScreenKey() function to create these keys.

    // Example of a Screen with arguments and a unique key
    @Parcelize
    class SampleScreen(val id: String) : Screen {
        override val key: ScreenKey = generateScreenKey()
    
        @Composable
        override fun Content(modifier: Modifier) {
            Text("Screen ID: $id", modifier = modifier)
        }
    }
  5. Control Screen Lifecycle via Transitions

    dev

    If a screen uses a ScreenTransition, it cannot reach the RESUMED state until the animation is finished.

    • Without transition: The state moves immediately from STARTED $\rightarrow$ RESUMED.
    • With transition: The state moves from STARTED $\rightarrow$ (animation playing) $\rightarrow$ RESUMED (only after animation completes).

    Because of parent-child coordination, a parent's transition animation will also delay its children from reaching the RESUMED state.

  6. How ContainerScreen and NavigationState work together

    dev

    A ContainerScreen is a specialized Screen that can host other nested screens. It is parameterized by a State type, which must implement NavigationState.

    NavigationState is a class that holds the current hierarchy of nested screens and any additional metadata. To ensure Modo can correctly manage the lifecycle and restoration of nested screens, your state must implement getChildScreens() and return a list of all nested Screen instances.

    Built-in implementations of ContainerScreen include StackScreen and MultiScreen.

    @Parcelize
    data class SampleState(
      val screen1: Screen,
      val screen2: Screen,
      val screen3: Screen?,
    ) : NavigationState {
      // You must return all nested screens to ensure correct functionality.
      override fun getChildScreens(): List<Screen> = listOfNotNull(screen1, screen2, screen3)
    }
  7. Understand the Screen Instance Lifecycle

    dev

    In Modo, screen instances have a long lifetime that is independent of the Activity or Fragment lifecycle. They survive recompositions and configuration changes (like rotation or language changes). This makes them safe to inject into Dependency Injection (DI) containers, provided the DI container's lifetime is shorter than or equal to the screen's lifetime. This behavior is guaranteed when using Modo.rememberRootScreen() and similar built-in functions.

    Each screen progresses through these standard states:

    • INITIALIZED: Instance created, but never displayed.
    • CREATED: Displayed at least once.
    • STARTED: Currently in composition.
    • RESUMED: Visible, ready for interaction, and all transitions/animations are complete.
    • DESTROYED: Removed from the navigation graph and resources are cleaned up.
  8. How Parent-Child Lifecycle Coordination works

    dev

    Modo enforces strict rules for nested screens to ensure UI consistency. A child screen's lifecycle state is always constrained by its parent's state.

    The Golden Rule: A child's lifecycle state never exceeds its parent's state.

    • If Parent is RESUMED, Child can reach RESUMED.
    • If Parent is STARTED, Child is blocked from reaching RESUMED (it can only reach STARTED).
    • If Parent is CREATED, Child is blocked from reaching STARTED (it can only reach CREATED).

    Event Propagation: When a screen enters composition, it subscribes to its parent. Events propagate as follows:

    • ON_START $\rightarrow$ Child moves to STARTED.
    • ON_RESUME $\rightarrow$ Child moves to RESUMED (only if transitions/animations are complete).
    • ON_PAUSE $\rightarrow$ Child immediately moves to STARTED.
    • ON_STOP $\rightarrow$ Child immediately moves to CREATED.
    • ON_DESTROY $\rightarrow$ Conditionally propagated (blocked during configuration changes to preserve SavedStateRegistry).
  9. Important rules for Gradle publishing

    dev

    When publishing Modo, adhere to these critical constraints to avoid failures:

    Single Gradle Session Requirement

    All publishing tasks must be run in a single command. If you run tasks separately, the staging repository ID (stored in Gradle's task state) will be lost, causing the second command to fail.

    # ❌ This will fail
    ./gradlew publishAllPublicationsToSonatypeRepository
    ./gradlew closeSonatypeStagingRepository
    
    # ✅ This works
    ./gradlew publishAllPublicationsToSonatypeRepository closeSonatypeStagingRepository

    Version Immutability

    Once a version is published to Maven Central, it is immutable. You cannot change, delete, or republish the same version. If a mistake is made, you must release a new version (e.g., incrementing the patch version) to fix it.

  10. How state defines UI in Modo

    dev

    In Modo, the UI is a direct reflection of the NavigationState.

    • Defining Initial State: The initial state of a ContainerScreen is defined via the navModel: NavModel<State> parameter in its constructor.
    • Updating State: To navigate or change the UI, you update the state. You can do this by:
      • Dispatching a lambda that calculates a new state based on the current state.
      • Using built-in extension functions provided for specific container types like StackScreen and MultiScreen.

    Because ContainerScreen implementations (like StackScreen or MultiScreen) manage child screens through their state, the UI automatically reacts to state transitions.

  11. Use ScreenModel for state management

    dev

    Modo provides ScreenModel, an interface similar to Android's ViewModel. It includes an onDispose function that is automatically called when the ScreenModel needs to be cleared (e.g., when the screen is removed from the navigation stack). To use a ScreenModel within a screen, call rememberScreenModel inside your @Composable Content function.

    // Inside your Screen's Content function
    val screenModel = rememberScreenModel<MyScreenModel>()