Refactor navigation state management

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.
pull/2003/head
Don Turner 2 weeks ago
parent ecb759d8fa
commit adcc3871be

@ -85,6 +85,8 @@ class MainActivity : ComponentActivity() {
private val viewModel: MainActivityViewModel by viewModels() private val viewModel: MainActivityViewModel by viewModels()
// TODO: This isn't used
private val backStackViewModel: NiaBackStackViewModel by viewModels() private val backStackViewModel: NiaBackStackViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

@ -17,7 +17,7 @@
package com.google.samples.apps.nowinandroid.di package com.google.samples.apps.nowinandroid.di
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigatorState import com.google.samples.apps.nowinandroid.core.navigation.NavigationState
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -28,15 +28,19 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.polymorphic
import javax.inject.Singleton import javax.inject.Singleton
// TODO: Rename to `NiaNavigationStateProvider`
// Does this even need to be injected? Can't we just instantiate it directly using `rememberNavigationState`?
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object NiaNavigatorProvider { object NavigationStateProvider {
@Provides @Provides
@Singleton @Singleton
fun providerNiaNavigatorState(): NiaNavigatorState = fun provideNavigationState(): NavigationState =
NiaNavigatorState( NavigationState(
startKey = TopLevelDestination.FOR_YOU.key, startKey = TopLevelDestination.FOR_YOU.key,
) )
// TODO: Remove commented out code
// //
// @Provides // @Provides
// @Singleton // @Singleton

@ -23,8 +23,7 @@ import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator import com.google.samples.apps.nowinandroid.core.navigation.NiaNavigator
import com.google.samples.apps.nowinandroid.core.navigation.getEntries import com.google.samples.apps.nowinandroid.core.navigation.toEntries
import kotlin.collections.plus
@OptIn(ExperimentalMaterial3AdaptiveApi::class) @OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable @Composable
@ -33,7 +32,7 @@ fun NiaNavDisplay(
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>, entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
) { ) {
val listDetailStrategy = rememberListDetailSceneStrategy<NiaNavKey>() val listDetailStrategy = rememberListDetailSceneStrategy<NiaNavKey>()
val entries = niaNavigator.navigatorState.getEntries(entryProviderBuilders) val entries = niaNavigator.navigationState.toEntries(entryProviderBuilders)
NavDisplay( NavDisplay(
entries = entries, entries = entries,
sceneStrategy = listDetailStrategy, sceneStrategy = listDetailStrategy,

@ -241,6 +241,11 @@ internal fun NiaApp(
}, },
), ),
) { ) {
// Instantiate the NavigationState here
NiaNavDisplay( NiaNavDisplay(
niaNavigator = appState.niaNavigator, niaNavigator = appState.niaNavigator,
entryProviderBuilders = entryProviderBuilders, entryProviderBuilders = entryProviderBuilders,

@ -71,7 +71,7 @@ class NiaAppState(
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
) { ) {
val currentTopLevelDestination: TopLevelDestination? val currentTopLevelDestination: TopLevelDestination?
@Composable get() = TopLevelDestinations[niaNavigator.navigatorState.currentTopLevelKey] @Composable get() = TopLevelDestinations[niaNavigator.navigationState.currentTopLevelKey]
val isOffline = networkMonitor.isOnline val isOffline = networkMonitor.isOnline
.map(Boolean::not) .map(Boolean::not)

@ -48,7 +48,7 @@ class NiaBackStackViewModelTest {
private fun createViewModel() = NiaBackStackViewModel( private fun createViewModel() = NiaBackStackViewModel(
savedStateHandle = SavedStateHandle(), savedStateHandle = SavedStateHandle(),
niaNavigatorState = NiaNavigatorState(TestStartKey), navigationState = NavigationState(TestStartKey),
serializersModules = serializersModules, serializersModules = serializersModules,
) )
@ -67,7 +67,7 @@ class NiaBackStackViewModelTest {
fun testNonTopLevelKeySaved() { fun testNonTopLevelKeySaved() {
val viewModel = createViewModel() val viewModel = createViewModel()
rule.setContent { rule.setContent {
val navigator = remember { NiaNavigator(viewModel.niaNavigatorState) } val navigator = remember { NiaNavigator(viewModel.navigationState) }
navigator.navigate(TestKeyFirst) navigator.navigate(TestKeyFirst)
} }
assertThat(viewModel.backStackMap.size).isEqualTo(1) assertThat(viewModel.backStackMap.size).isEqualTo(1)
@ -80,7 +80,7 @@ class NiaBackStackViewModelTest {
fun testTopLevelKeySaved() { fun testTopLevelKeySaved() {
val viewModel = createViewModel() val viewModel = createViewModel()
rule.setContent { rule.setContent {
val navigator = remember { NiaNavigator(viewModel.niaNavigatorState) } val navigator = remember { NiaNavigator(viewModel.navigationState) }
navigator.navigate(TestKeyFirst) navigator.navigate(TestKeyFirst)
navigator.navigate(TestTopLevelKeyFirst) navigator.navigate(TestTopLevelKeyFirst)
@ -101,7 +101,7 @@ class NiaBackStackViewModelTest {
fun testMultiStacksSaved() { fun testMultiStacksSaved() {
val viewModel = createViewModel() val viewModel = createViewModel()
rule.setContent { rule.setContent {
val navigator = remember { NiaNavigator(viewModel.niaNavigatorState) } val navigator = remember { NiaNavigator(viewModel.navigationState) }
navigator.navigate(TestKeyFirst) navigator.navigate(TestKeyFirst)
navigator.navigate(TestTopLevelKeyFirst) navigator.navigate(TestTopLevelKeyFirst)
navigator.navigate(TestKeySecond) navigator.navigate(TestKeySecond)
@ -122,7 +122,7 @@ class NiaBackStackViewModelTest {
fun testPopSaved() { fun testPopSaved() {
val viewModel = createViewModel() val viewModel = createViewModel()
rule.setContent { rule.setContent {
val navigator = remember { NiaNavigator(viewModel.niaNavigatorState) } val navigator = remember { NiaNavigator(viewModel.navigationState) }
navigator.navigate(TestKeyFirst) navigator.navigate(TestKeyFirst)
@ -144,14 +144,14 @@ class NiaBackStackViewModelTest {
fun testRestore() { fun testRestore() {
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel> lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
lateinit var navigator: NiaNavigator lateinit var navigator: NiaNavigator
lateinit var navigatorState: NiaNavigatorState lateinit var navigatorState: NavigationState
rule.setContent { rule.setContent {
navigatorState = remember { NiaNavigatorState(TestStartKey) } navigatorState = remember { NavigationState(TestStartKey) }
navigator = remember { NiaNavigator(navigatorState) } navigator = remember { NiaNavigator(navigatorState) }
scenario = viewModelScenario { scenario = viewModelScenario {
NiaBackStackViewModel( NiaBackStackViewModel(
savedStateHandle = createSavedStateHandle(), savedStateHandle = createSavedStateHandle(),
niaNavigatorState = navigatorState, navigationState = navigatorState,
serializersModules = serializersModules, serializersModules = serializersModules,
) )
} }
@ -181,14 +181,14 @@ class NiaBackStackViewModelTest {
fun testRestoreMultiStacks() { fun testRestoreMultiStacks() {
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel> lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
lateinit var navigator: NiaNavigator lateinit var navigator: NiaNavigator
lateinit var navigatorState: NiaNavigatorState lateinit var navigatorState: NavigationState
rule.setContent { rule.setContent {
navigatorState = remember { NiaNavigatorState(TestStartKey) } navigatorState = remember { NavigationState(TestStartKey) }
navigator = remember { NiaNavigator(navigatorState) } navigator = remember { NiaNavigator(navigatorState) }
scenario = viewModelScenario { scenario = viewModelScenario {
NiaBackStackViewModel( NiaBackStackViewModel(
savedStateHandle = createSavedStateHandle(), savedStateHandle = createSavedStateHandle(),
niaNavigatorState = navigatorState, navigationState = navigatorState,
serializersModules = serializersModules, serializersModules = serializersModules,
) )
} }

@ -33,10 +33,16 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import javax.inject.Inject import javax.inject.Inject
/**
* TODO: I'm not sure why this needs to be a ViewModel - why can't it be a plain state holder that
* is scoped to `NiaAppState`?
* https://github.com/android/nav3-recipes/blob/main/app/src/main/java/com/example/nav3recipes/multiplestacks/NavigationState.kt#L71
*
*/
@HiltViewModel @HiltViewModel
class NiaBackStackViewModel @Inject constructor( class NiaBackStackViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
val niaNavigatorState: NiaNavigatorState, val navigationState: NavigationState,
serializersModules: SerializersModule, serializersModules: SerializersModule,
) : ViewModel() { ) : ViewModel() {
@ -63,9 +69,9 @@ class NiaBackStackViewModel @Inject constructor(
init { init {
if (backStackMap.isNotEmpty()) { if (backStackMap.isNotEmpty()) {
// Restore backstack from saved state handle if not emtpy // Restore backstack from saved state handle if not empty
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
niaNavigatorState.restore( navigationState.restore(
activeTopLeveLKeys, activeTopLeveLKeys,
backStackMap as LinkedHashMap<NiaNavKey, SnapshotStateList<NiaNavKey>>, backStackMap as LinkedHashMap<NiaNavKey, SnapshotStateList<NiaNavKey>>,
) )
@ -74,8 +80,8 @@ class NiaBackStackViewModel @Inject constructor(
// Start observing changes to the backStack and save backStack whenever it updates // Start observing changes to the backStack and save backStack whenever it updates
viewModelScope.launch { viewModelScope.launch {
snapshotFlow { snapshotFlow {
activeTopLeveLKeys = niaNavigatorState.activeTopLeveLKeys.toList() activeTopLeveLKeys = navigationState.activeTopLeveLKeys.toList()
backStackMap = niaNavigatorState.backStacks backStackMap = navigationState.backStacks
}.collect() }.collect()
} }
} }

@ -34,7 +34,8 @@ import org.jetbrains.annotations.VisibleForTesting
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.plus import kotlin.collections.plus
class NiaNavigatorState( // TODO: Consider changing this to `NiaNavigationState`
class NavigationState(
internal val startKey: NiaNavKey, internal val startKey: NiaNavKey,
) { ) {
internal var backStacks: MutableMap<NiaNavKey, SnapshotStateList<NiaNavKey>> = internal var backStacks: MutableMap<NiaNavKey, SnapshotStateList<NiaNavKey>> =
@ -51,7 +52,7 @@ class NiaNavigatorState(
val currentBackStack: List<NiaNavKey> val currentBackStack: List<NiaNavKey>
get() = activeTopLeveLKeys.fold(mutableListOf()) { list, topLevelKey -> get() = activeTopLeveLKeys.fold(mutableListOf()) { list, topLevelKey ->
list.apply { list.apply {
addAll(backStacks[topLevelKey]!!) addAll(backStacks[topLevelKey] ?: error("No back stack found for $topLevelKey"))
} }
} }
@ -76,13 +77,18 @@ class NiaNavigatorState(
} }
} }
// https://github.com/android/nowinandroid/issues/1934 /**
* TODO: Document this
*/
class NiaNavigator @Inject constructor( class NiaNavigator @Inject constructor(
val navigatorState: NiaNavigatorState, val navigationState: NavigationState,
) { ) {
// TODO: I wonder if it'd be simpler to have separate methods
// for navigating to a graph and navigating to a key. If the key is on a separate graph then
// navigate to that graph first.
fun navigate(key: NiaNavKey) { fun navigate(key: NiaNavKey) {
val currentActiveSubStacks = linkedSetOf<NiaNavKey>() val currentActiveSubStacks = linkedSetOf<NiaNavKey>()
navigatorState.apply { navigationState.apply {
currentActiveSubStacks.addAll(activeTopLeveLKeys) currentActiveSubStacks.addAll(activeTopLeveLKeys)
when { when {
// top level singleTop -> clear substack // top level singleTop -> clear substack
@ -120,7 +126,7 @@ class NiaNavigator @Inject constructor(
} }
fun pop() { fun pop() {
navigatorState.apply { navigationState.apply {
val currentSubstack = backStacks[currentTopLevelKey]!! val currentSubstack = backStacks[currentTopLevelKey]!!
if (currentSubstack.size == 1) { if (currentSubstack.size == 1) {
// if current sub-stack only has one key, remove the sub-stack from the map // if current sub-stack only has one key, remove the sub-stack from the map
@ -134,12 +140,17 @@ class NiaNavigator @Inject constructor(
} }
} }
// TODO: I wonder if removing this would remove the need for the serializers modules
interface NiaNavKey { interface NiaNavKey {
val isTopLevel: Boolean val isTopLevel: Boolean
} }
/**
* Convert the navigation state to `NavEntry`s that can be displayed by a `NavDisplay`
*/
@Composable @Composable
public fun NiaNavigatorState.getEntries( fun NavigationState.toEntries(
// TODO: Might be better to pass this in fully constructed
entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>, entryProviderBuilders: Set<EntryProviderScope<NiaNavKey>.() -> Unit>,
): List<NavEntry<NiaNavKey>> = ): List<NavEntry<NiaNavKey>> =
activeTopLeveLKeys.fold(emptyList()) { entries, topLevelKey -> activeTopLeveLKeys.fold(emptyList()) { entries, topLevelKey ->

@ -24,49 +24,49 @@ import kotlin.test.assertFailsWith
class NiaNavigatorStateTest { class NiaNavigatorStateTest {
private lateinit var niaNavigatorState: NiaNavigatorState private lateinit var navigationState: NavigationState
private lateinit var niaNavigator: NiaNavigator private lateinit var niaNavigator: NiaNavigator
@Before @Before
fun setup() { fun setup() {
niaNavigatorState = NiaNavigatorState(TestStartKey) navigationState = NavigationState(TestStartKey)
niaNavigator = NiaNavigator(niaNavigatorState) niaNavigator = NiaNavigator(navigationState)
} }
@Test @Test
fun testStartKey() { fun testStartKey() {
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey) assertThat(navigationState.currentKey).isEqualTo(TestStartKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test
fun testNavigate() { fun testNavigate() {
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst) assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test
fun testNavigateTopLevel() { fun testNavigateTopLevel() {
niaNavigator.navigate(TestTopLevelKey) niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
} }
@Test @Test
fun testNavigateSingleTop() { fun testNavigateSingleTop() {
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
).inOrder() ).inOrder()
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
).inOrder() ).inOrder()
@ -77,7 +77,7 @@ class NiaNavigatorStateTest {
niaNavigator.navigate(TestTopLevelKey) niaNavigator.navigate(TestTopLevelKey)
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestTopLevelKey, TestTopLevelKey,
TestKeyFirst, TestKeyFirst,
@ -85,7 +85,7 @@ class NiaNavigatorStateTest {
niaNavigator.navigate(TestTopLevelKey) niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestTopLevelKey, TestTopLevelKey,
).inOrder() ).inOrder()
@ -95,13 +95,13 @@ class NiaNavigatorStateTest {
fun testSubStack() { fun testSubStack() {
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst) assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
niaNavigator.navigate(TestKeySecond) niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond) assertThat(navigationState.currentKey).isEqualTo(TestKeySecond)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test
@ -109,33 +109,33 @@ class NiaNavigatorStateTest {
// add to start stack // add to start stack
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst) assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
// navigate to new top level // navigate to new top level
niaNavigator.navigate(TestTopLevelKey) niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// add to new stack // add to new stack
niaNavigator.navigate(TestKeySecond) niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond) assertThat(navigationState.currentKey).isEqualTo(TestKeySecond)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// go back to start stack // go back to start stack
niaNavigator.navigate(TestStartKey) niaNavigator.navigate(TestStartKey)
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst) assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test
fun testRestore() { fun testRestore() {
assertThat(niaNavigatorState.currentBackStack).containsExactly(TestStartKey) assertThat(navigationState.currentBackStack).containsExactly(TestStartKey)
niaNavigatorState.restore( navigationState.restore(
listOf(TestStartKey, TestTopLevelKey), listOf(TestStartKey, TestTopLevelKey),
linkedMapOf( linkedMapOf(
TestStartKey to mutableStateListOf(TestStartKey, TestKeyFirst), TestStartKey to mutableStateListOf(TestStartKey, TestKeyFirst),
@ -143,15 +143,15 @@ class NiaNavigatorStateTest {
), ),
) )
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
TestTopLevelKey, TestTopLevelKey,
TestKeySecond, TestKeySecond,
).inOrder() ).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeySecond) assertThat(navigationState.currentKey).isEqualTo(TestKeySecond)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
} }
@Test @Test
@ -159,7 +159,7 @@ class NiaNavigatorStateTest {
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
niaNavigator.navigate(TestKeySecond) niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
TestKeySecond, TestKeySecond,
@ -167,13 +167,13 @@ class NiaNavigatorStateTest {
niaNavigator.pop() niaNavigator.pop()
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
).inOrder() ).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst) assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test
@ -181,25 +181,25 @@ class NiaNavigatorStateTest {
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
niaNavigator.navigate(TestTopLevelKey) niaNavigator.navigate(TestTopLevelKey)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
TestTopLevelKey, TestTopLevelKey,
).inOrder() ).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentKey).isEqualTo(TestTopLevelKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestTopLevelKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestTopLevelKey)
// remove TopLevel // remove TopLevel
niaNavigator.pop() niaNavigator.pop()
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
).inOrder() ).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestKeyFirst) assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test
@ -207,7 +207,7 @@ class NiaNavigatorStateTest {
niaNavigator.navigate(TestKeyFirst) niaNavigator.navigate(TestKeyFirst)
niaNavigator.navigate(TestKeySecond) niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestKeyFirst, TestKeyFirst,
TestKeySecond, TestKeySecond,
@ -216,12 +216,12 @@ class NiaNavigatorStateTest {
niaNavigator.pop() niaNavigator.pop()
niaNavigator.pop() niaNavigator.pop()
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
).inOrder() ).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey) assertThat(navigationState.currentKey).isEqualTo(TestStartKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test
@ -238,7 +238,7 @@ class NiaNavigatorStateTest {
niaNavigator.navigate(testTopLevelKeyTwo) niaNavigator.navigate(testTopLevelKeyTwo)
niaNavigator.navigate(TestKeySecond) niaNavigator.navigate(TestKeySecond)
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
TestTopLevelKey, TestTopLevelKey,
TestKeyFirst, TestKeyFirst,
@ -250,12 +250,12 @@ class NiaNavigatorStateTest {
niaNavigator.pop() niaNavigator.pop()
} }
assertThat(niaNavigatorState.currentBackStack).containsExactly( assertThat(navigationState.currentBackStack).containsExactly(
TestStartKey, TestStartKey,
).inOrder() ).inOrder()
assertThat(niaNavigatorState.currentKey).isEqualTo(TestStartKey) assertThat(navigationState.currentKey).isEqualTo(TestStartKey)
assertThat(niaNavigatorState.currentTopLevelKey).isEqualTo(TestStartKey) assertThat(navigationState.currentTopLevelKey).isEqualTo(TestStartKey)
} }
@Test @Test

@ -39,11 +39,16 @@ object ForYouEntryProvider {
@IntoSet @IntoSet
fun provideForYouEntryProviderBuilder( fun provideForYouEntryProviderBuilder(
navigator: NiaNavigator, navigator: NiaNavigator,
): EntryProviderScope<NiaNavKey>.() -> Unit = { ): EntryProviderScope<NiaNavKey>.() -> Unit = forYouEntry(navigator)
entry<ForYouRoute> { }
ForYouScreen(
onTopicClick = navigator::navigateToTopic, fun forYouEntry(navigator: NiaNavigator): EntryProviderScope<NiaNavKey>.() -> Unit = {
) entry<ForYouRoute> {
} ForYouScreen(
onTopicClick = navigator::navigateToTopic,
)
} }
} }

@ -47,6 +47,7 @@ fun InterestsScreen(
uiState = uiState, uiState = uiState,
followTopic = viewModel::followTopic, followTopic = viewModel::followTopic,
onTopicClick = { onTopicClick = {
// TODO: this violates SSOT, events should go through the ViewModel
viewModel.onTopicClick(it) viewModel.onTopicClick(it)
onTopicClick(it) onTopicClick(it)
}, },

@ -39,9 +39,12 @@ class InterestsViewModel @AssistedInject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
val userDataRepository: UserDataRepository, val userDataRepository: UserDataRepository,
getFollowableTopics: GetFollowableTopicsUseCase, getFollowableTopics: GetFollowableTopicsUseCase,
// TODO: see comment below
@Assisted val key: InterestsRoute, @Assisted val key: InterestsRoute,
) : ViewModel() { ) : ViewModel() {
// TODO: this should no longer be necessary, the currently selected topic should be
// available through the navigation state
// Key used to save and retrieve the currently selected topic id from saved state. // Key used to save and retrieve the currently selected topic id from saved state.
private val selectedTopicIdKey = "selectedTopicIdKey" private val selectedTopicIdKey = "selectedTopicIdKey"
@ -67,6 +70,8 @@ class InterestsViewModel @AssistedInject constructor(
} }
fun onTopicClick(topicId: String?) { fun onTopicClick(topicId: String?) {
// TODO: This should modify the navigation state directly rather than just updating the
// savedStateHandle
savedStateHandle[selectedTopicIdKey] = topicId savedStateHandle[selectedTopicIdKey] = topicId
} }

@ -52,7 +52,11 @@ object InterestsEntryProvider {
it.create(key) it.create(key)
} }
InterestsScreen( InterestsScreen(
// TODO: This event should be provided by the ViewModel
onTopicClick = navigator::navigateToTopic, onTopicClick = navigator::navigateToTopic,
// TODO: This should be dynamically calculated based on the rendering scene
// See https://github.com/android/nav3-recipes/commit/488f4811791ca3ed7192f4fe3c86e7371b32ebdc#diff-374e02026cdd2f68057dd940f203dc4ba7319930b33e9555c61af7e072211cabR89
shouldHighlightSelectedTopic = false, shouldHighlightSelectedTopic = false,
viewModel = viewModel, viewModel = viewModel,
) )

@ -162,6 +162,7 @@ class InterestsListDetailScreenTest {
composeTestRule.apply { composeTestRule.apply {
setContent { setContent {
val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>() val backStackViewModel by composeTestRule.activity.viewModels<NiaBackStackViewModel>()
// TODO: This is broken
val backStack = backStackViewModel.niaNavigator.backStack val backStack = backStackViewModel.niaNavigator.backStack
NiaTheme { NiaTheme {
NavDisplay( NavDisplay(

Loading…
Cancel
Save