Skip to content

Conversation

@dturner
Copy link
Collaborator

@dturner dturner commented Dec 2, 2025

This PR migrates the project from using Navigation Compose (aka Nav2) to Navigation 3. It follows the official migration guide.

Implementation details

Navigable content
Every piece of navigable content in the app is represented by a navigation key. These are objects that implement the NavKey interface. Each feature has an api module which defines these navigation keys.

Example from the :foryou:api module:

data object ForYouNavKey : NavKey

The content itself is defined using extension functions on EntryProviderScope<NavKey>. These extension functions are defined by each feature in its impl module.

Example from the :foryou:impl module:

fun EntryProviderScope<NavKey>.forYouEntry(navigator: Navigator) {
    entry<ForYouNavKey> {
        ForYouScreen(
            onTopicClick = navigator::navigateToTopic,
        )
    }
}

The app (specifically the NiaApp composable) calls these extension functions directly to create an entryProvider. This is used to resolve the navigation keys to NavEntrys which contain the navigable content:

val entryProvider = entryProvider {
    forYouEntry(navigator)
    bookmarksEntry(navigator)
    interestsEntry(navigator)
    topicEntry(navigator)
    searchEntry(navigator)
}

Modeling state
Navigation state is held using the NavigationState class and scoped to NiaApp. This is a state holder, it does not create its own state. The navigation state - a top level back stack and sub stacks for each top level key - is created using rememberNavigationState. This uses Nav3's rememberNavBackStack helper function to allow that state to persist config changes and process death.

Handling navigation events
Navigation events - navigate and goBack - are handled using the Navigator class. This modifies the NavigationState in response to these events. The Navigator is also scoped to NiaApp and passed directly to each feature's EntryProviderScope extension functions so that the content can trigger navigation events.

Updating the UI
NavigationState has an extension function toEntries that uses the entryProvider to convert its state into NavEntrys. These are then displayed using a NavDisplay inside the NiaApp composable. Any changes to the navigation state will cause NavDisplay to recompose and update its UI.

Notably, toEntries calls rememberDecoratedNavEntries for each sub stack. This allows the state of each sub stack to be retained, even when that sub stack isn't in the current stack being displayed by NavDisplay. This is so that when you tap on a previously visited top level destination, the state of all those screens is retained.

Summary of data flow

  • Navigation events flow into Navigator
  • Navigator changes NavigatorState
  • NavigatorState.toEntries recomposes, recreating the list of NavEntrys
  • NavDisplay recomposes, displaying those NavEntrys

Notable points

  • Dependency injection is not used in this navigation architecture. The number of screens is sufficiently small that the extra complexity does not provide sufficient benefit. This might change in future. If you have a lot of screens, check out these architecture recipes.

This PR builds on the work done by @claraf3 in #1902

claraf3 and others added 24 commits November 19, 2025 15:07
Source code is still left in api module. impl module is empty at this point.
Bump agp versions and add navigation 3 dependency
Made NiaNavigator a stateless class only responsibly for navigating and pop (modifying backStack).
Navigation state now lives in a new class called NiaNavigatorState.

The state of this class is saved and restored by ViewModel.
This commit refactors the navigation state management by renaming `NiaNavigatorState` to `NavigationState` to make it more generic.

Specific changes include:
- Renamed `NiaNavigatorState` to `NavigationState`.
- Renamed `NiaNavigatorProvider` to `NavigationStateProvider`.
- Updated all usages of the renamed classes, including `NiaNavigator`, `NiaBackStackViewModel`, and various tests.
- Replaced the `getEntries()` extension function with `toEntries()`.
- Added numerous TODOs to identify areas for future improvement, such as removing dependencies on `SavedStateHandle` for navigation state, simplifying event handling in ViewModels, and documenting the new navigation components.
This commit refactors the navigation implementation by renaming all `...Route` classes to `...NavKey`. This change provides more descriptive and consistent naming for navigation keys across the codebase.

Key changes include:
*   Renamed `BookmarksRoute` to `BookmarksNavKey`
*   Renamed `ForYouRoute` to `ForYouNavKey`
*   Renamed `InterestsRoute` to `InterestsNavKey`
*   Renamed `TopicRoute` to `TopicNavKey`
*   Renamed `SearchRoute` to `SearchNavKey`
*   Updated all associated feature modules, tests, and UI components to use the new `NavKey` names.
*   Removed obsolete test utilities and mock providers related to the old navigation setup.
*   Deleted outdated dependency graph images and their corresponding `README.md` files from feature modules.
Adds automatically generated dependency graphs to the README.md files for the following feature modules:
- `:feature:bookmarks:api`
- `:feature:bookmarks:impl`
- `:feature:foryou:api`
- `:feature:foryou:impl`
The `NiaAppStateTest` is updated to align with the changes in `InterestsNavKey`, which now requires a `topicId` argument. The test now uses `InterestsNavKey(null)` to correctly represent the top-level interests destination.
The `feature:settings:api` module has been renamed to `feature:settings:impl` to better reflect its role as an implementation module.

This change includes:
*   Updating module paths and namespaces.
*   Moving all related files, including source code, resources, and tests, to the new `impl` directory.
*   Updating dependencies and project configurations in Gradle scripts and README files to point to the new module path.
*   Renaming string resources to include the `_impl` suffix for clarity.
*   Updating the AndroidX Lifecycle dependency to version 2.10.0.
@dturner
Copy link
Collaborator Author

dturner commented Dec 2, 2025

@SimonMarquis I'm running into an issue where the checkGraphs build step is failing: https://github.com/android/nowinandroid/actions/runs/19862889898/job/56917497454#step:13:21

Any idea what could be causing that?

Additionally, when I run graphUpdate locally it doesn't generate READMEs for all the modules. Might be related to the above. Any help greatly appreciated here.

The string resource identifiers for settings have been updated in the navigation tests. The `feature_settings` prefix has been changed to `feature_settings_impl` to reflect changes in the settings feature module.
Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android Lint found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

This commit updates the `targetSdkVersion` and `compileSdkVersion` from 35 to 36.
@SimonMarquis
Copy link
Contributor

@dturner the same commands seems to work as expected when I checkout eae7a5b on a GitHub Codespace, so I'm not sure what really happens.

Also, I'd just like to point out that currently, the Graph generation does not support multiple nested groups

// Nodes and subgraphs (limited to a single nested layer)

And this makes the ":feature" group disappear as you can see in

---
config:
  layout: elk
  elk:
    nodePlacementStrategy: SIMPLE
---
graph TB
  subgraph :core
    direction TB
    :core:analytics[analytics]:::android-library
    :core:common[common]:::jvm-library
    :core:data[data]:::android-library
    :core:database[database]:::android-library
    :core:datastore[datastore]:::android-library
    :core:datastore-proto[datastore-proto]:::android-library
    :core:designsystem[designsystem]:::android-library
    :core:model[model]:::jvm-library
    :core:navigation[navigation]:::android-library
    :core:network[network]:::android-library
    :core:notifications[notifications]:::android-library
    :core:ui[ui]:::android-library
  end
  subgraph :feature:bookmarks
    direction TB
    :feature:bookmarks:api[api]:::android-library
    :feature:bookmarks:impl[impl]:::android-library
  end
  subgraph :feature:topic
    direction TB
    :feature:topic:api[api]:::android-library
  end

  :core:data -.-> :core:analytics
  :core:data --> :core:common
  :core:data --> :core:database
  :core:data --> :core:datastore
  :core:data --> :core:network
  :core:data -.-> :core:notifications
  :core:database --> :core:model
  :core:datastore -.-> :core:common
  :core:datastore --> :core:datastore-proto
  :core:datastore --> :core:model
  :core:network --> :core:common
  :core:network --> :core:model
  :core:notifications -.-> :core:common
  :core:notifications --> :core:model
  :core:ui --> :core:analytics
  :core:ui --> :core:designsystem
  :core:ui --> :core:model
  :feature:bookmarks:api --> :core:navigation
  :feature:bookmarks:impl -.-> :core:data
  :feature:bookmarks:impl -.-> :core:designsystem
  :feature:bookmarks:impl -.-> :core:ui
  :feature:bookmarks:impl -.-> :feature:bookmarks:api
  :feature:bookmarks:impl -.-> :feature:topic:api
  :feature:topic:api -.-> :core:designsystem
  :feature:topic:api --> :core:navigation
  :feature:topic:api -.-> :core:ui

classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;

Loading

Updated string resource identifiers in the `SettingsDialogTest` to follow the `feature_settings_impl_` prefix convention. This aligns the test resources with the recent module renaming.
@SimonMarquis
Copy link
Contributor

I don't have much time to dedicate to adding support for nested projects right now, but maybe this could be a good fit for a vibe coding task 🤓

@dturner
Copy link
Collaborator Author

dturner commented Dec 2, 2025

@SimonMarquis Thanks for the quick response. No worries, I'll see what Gemini can do...

Renamed the `goUp` method to `clearSubStack` to more accurately reflect that its function is to clear the sub-stack.
Add `rememberViewModelStoreNavEntryDecorator` to the list of decorators for `NavEntry` instances. This enables support for `ViewModel` instances within the navigation component.
@ianhanniballake ianhanniballake self-requested a review December 3, 2025 17:07
@dturner dturner merged commit 161181f into main Dec 3, 2025
5 checks passed
@dturner dturner deleted the dt/nav3-migration branch December 3, 2025 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants