What is Modo and how does it work?
devNavigationState, 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.repository·dev·Indexed 43 days ago
https://github.com/ikarenkov/modoA 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.
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.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." }
}
}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:
Screens or ContainerScreens within their NavigationState.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.
ViewModel. You can integrate existing Android ViewModel logic into your Modo screens just as you would in a standard Compose application.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)
}
}If a screen uses a ScreenTransition, it cannot reach the RESUMED state until the animation is finished.
STARTED $\rightarrow$ RESUMED.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.
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)
}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:
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.
RESUMED, Child can reach RESUMED.STARTED, Child is blocked from reaching RESUMED (it can only reach STARTED).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).When publishing Modo, adhere to these critical constraints to avoid failures:
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 closeSonatypeStagingRepositoryOnce 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.
In Modo, the UI is a direct reflection of the NavigationState.
ContainerScreen is defined via the navModel: NavModel<State> parameter in its constructor.StackScreen and MultiScreen.Because ContainerScreen implementations (like StackScreen or MultiScreen) manage child screens through their state, the UI automatically reacts to state transitions.
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>()