Architecture refactor: decouple NavModel, renderer, and reducer pipeline#78
Open
ikarenkov wants to merge 25 commits into
Open
Architecture refactor: decouple NavModel, renderer, and reducer pipeline#78ikarenkov wants to merge 25 commits into
ikarenkov wants to merge 25 commits into
Conversation
…s. Removed the `NavigationRenderer` interface and simplified `NavModel` to hold state in a `MutableStateFlow`. Updated `ComposeRenderer` to collect state changes within a managed `CoroutineScope`. Changed `LocalStackNavigation` and `LocalMultiScreenNavigation` to provide screen instances directly, and updated samples and tests to reflect these changes. Added coroutine testing support to the compose module.
…nsition to a reducer-based state update model. - Refactor `ContainerScreen`, `NavModel`, and `NavigationContainer` to remove the `Action` generic type parameter, leaving only `State`. - Introduce `NavigationReducer` as a functional interface for state transformations (State -> State) and make it the primary mechanism for `dispatch`. - Deprecate `NavigationAction` and container-specific action interfaces (e.g., `StackAction`, `MultiScreenAction`, `ListNavigationAction`) in favor of reducers. - Update `StackScreen`, `MultiScreen`, and `ListNavigationContainer` to align with the single-generic parameter. - Implement atomic dispatch for multiple reducers to ensure consistent state transitions. - Update `Modo` utility functions and `RootScreen` to support modern Android `getParcelable` APIs (SDK 33+). - Migrate sample applications and internal library components to use the new reducer-based `dispatch` pattern. - Add unit tests for `ComposeRenderer` to ensure coroutine scope disposal when screens are removed. - Rename action files to reducer files (e.g., `ListNavigationAction.kt` to `ListReducer.kt`) to match the new architecture.
…teFlow` for deep tree observation. - Renamed `NavigationContainer.navigationStateFlow` to `stateFlow` for brevity and consistency across the library. - Introduced `subtreeStateFlow()` extension to support observing navigation state changes across an entire hierarchy of containers. - Added a deprecated `navigationStateFlow()` extension with `DeprecationLevel.ERROR` to provide a migration path to the new API. - Added a `NavigationTreeStrip` component to the sample app to visualize the live navigation tree using the new subtree observation logic. - Updated `ComposeRenderer`, tests, and sample screens to reflect the API changes.
…Dialog to configure navigation tree visibility and display limits. Refactored NavigationTreeStrip to support AnimatedContent and dynamic visibility, and moved its placement to the root activity. Added maxLines support to LifecycleEventsHistory.
…duced hot `subtreeStateFlow` with deep tree observation. Updated `NavigationTree` logic and tests accordingly.
There was a problem hiding this comment.
detekt found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
| minSdk = "21" | ||
| compileSdk = "36" | ||
| koin = "4.0.0" | ||
| datastorePreferences = "1.1.1" |
| debug-logcat = { group = "com.squareup.logcat", name = "logcat", version = "0.1" } | ||
|
|
||
| kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "1.8.1" } | ||
| kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version = "1.8.1" } |
…ionState` and `navigationStateFlow` properties
added 13 commits
May 25, 2026 10:26
81a92e4 to
7358c2b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Architecture refactor
Before this PR the navigation layers were tightly coupled:
ContainerScreencarried both a model and aNavigationRenderer, the renderer interface was public, and state updates flowed through an action-dispatch protocol whereNavigationActionandReducerActionwere separate concepts (action = command; reducer =(action, state) -> State?). Together they forced model and renderer to share anActionvocabulary — hence the two-generic types.This refactor separates the concerns cleanly:
NavModelis a pure-Kotlin model.MutableStateFlow<State>plusdispatch(reducer). Noandroidx.compose.runtimereferences at the declaration level (stability is inherited fromNavigationContainer).Parcelable, so it survives process death on its own.ComposeRendereris an internal Compose adapter. It subscribes to aStateFlow<State>via a managedCoroutineScopeand mirrors the value into Compose state. It does not own state and does not know what produced it. The publicNavigationRendererinterface is gone.ContainerScreenis justScreen, NavigationContainer<State> by navModel. A screen with a model glued in via delegation, plus an internal renderer. No reducer plumbing or renderer indirection.NavigationReduceris now the single primitive:fun interface NavigationReducer<State> { fun reduce(oldState: State): State }. It replacesNavigationAction(the command-style marker) entirely — the dispatched reducer is the command. The oldReducerAction<State>survives only as a deprecated typealias toNavigationReducer<State>. Collapsing the two concepts is what lets theActiongeneric disappear.Everything else in this PR — single-generic types,
subtreeStateFlow, the deprecations — falls out of those new boundaries.API changes
Generics collapsed to a single type parameter.
NavigationContainer,ContainerScreen, andNavModelno longer have anActiongeneric. Call sites must drop the second type parameter:ContainerScreen<State, Action>→ContainerScreen<State>NavigationContainer<State, Action>→NavigationContainer<State>NavModel<State, Action>→NavModel<State>Action-dispatch model replaced with pure reducers.
NavigationReduceris now a single-methodfun interface:dispatch(...)accepts a reducer directly; state updates are pure functions of the previous state with no action argument.navigationStateremoved; state exposed as aStateFlow.NavModelholds state in aMutableStateFlow<State>, so observers and synchronous readers go throughstateFlow.value.Deep tree observation. Two new APIs let callers observe changes anywhere in the navigation subtree under a container:
subtreeFlow(): Flow<NavigationState>— cold flow that emits the root state on collection and re-emits whenever any descendant container dispatches.subtreeStateFlow(scope, started = Eagerly): StateFlow<NavigationState>— hot variant with synchronously accessible.value. Intentionally does not deduplicate same-value emissions, so nested dispatches that produce an unchanged root reference still notify observers to re-walk the tree.NavigationRendererinterface removed.ComposeRendereris internal and now owns aCoroutineScope(created ininit, cancelled ondispose) that collects state from the underlying flow.NavModelisParcelable. State now survives process death via the state-flow holder; no separate render layer is required.Deprecations
The previous action-based / hot-flow API is retained as deprecated bindings so existing code keeps compiling under transitional builds, but most are at
ERRORlevel — they will not run.NavigationActionand its subtypes (StackAction,MultiScreenAction,ListNavigationAction, etc.) — deprecated. Replace with reducer factories (StackActions,MultiScreenActions,ListReducer, the newRemoveTabReducer, …).NavigationContainer.dispatch(action: Action)— deprecated. Usedispatch(reducer: NavigationReducer<State>).NavigationContainer.navigationStateproperty — removed. ReadstateFlow.value.NavigationContainer.navigationStateFlow()extension — deprecated atERRORlevel (no runtime fallback). Migration path:stateFlowsubtreeFlow()orsubtreeStateFlow(scope)NavigationRendererinterface — removed. The renderer is internal toComposeRenderer; downstream code should not depend on it.NavigationReducer<State, Action>(two-generic form) — removed. Use the single-genericfun interface NavigationReducer<State>.Migration cheat sheet
Actiongeneric fromContainerScreen/NavigationContainer/NavModeldeclarations and references.NavigationReducer { oldState -> /* compute new state */ }.container.navigationState→container.stateFlow.value.container.navigationStateFlow()→container.stateFlow(per-container) orcontainer.subtreeStateFlow(scope)(deep observation).NavigationRendererinterface with the hot/coldStateFlowAPIs above.Sample app changes
SampleAppSettings— DataStore-backed settings holder. PersistsshowNavigationTree(Boolean) andnavTreeVisibleScreens(Int, default2).SettingsDialog— new dialog to toggle tree visibility and adjust depth via sliders.NavigationTree— new composable that replaces the previousNavigationTreeStrip. Driven bysubtreeStateFlow(), mounted at the root activity, wrapped inAnimatedContentwith dynamic visibility tied to settings.LifecycleEventsHistory— addedmaxLinessupport to bound the displayed event log.RemoveTabReducer— extracted reducer that replaces the previousRemoveTabActionin the multi-screen sample.Test changes
ComposeRendererDisposalTest— verifies the renderer'sCoroutineScopelifecycle (created on init, cancelled on dispose).DeepNavigationStateFlowTest— coverssubtreeStateFlowacross nested containers, including the deliberate no-op re-emission when a descendant dispatches without changing the root reference.ListNavigationActionAddScreensTest→ListReducerAddScreensTestListNavigationActionRemoveScreensTest→ListReducerRemoveScreensTestListNavigationActionSetTest→ListReducerSetTestRepo meta
AGENTS.mdandCLAUDE.mdfor AI-agent orientation..claude/skills/task-workflow/SKILL.mdfor the task-folder workflow.Test plan
./gradlew :modo-compose:test(incl.DeepNavigationStateFlowTest,ComposeRendererDisposalTest, renamedListReducer*Tests)./gradlew buildsucceeds across all modulesSettingsDialog, toggle tree visibility and depth, confirm both persist across app restartNavigationTreeshows/hides correctly from the root-activity mount and animates viaAnimatedContent