@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.compose.NavHost
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.bookmarksScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.forYouSection
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.navigateToInterests
|
||||
import com.google.samples.apps.nowinandroid.feature.search.api.navigation.searchScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.topicScreen
|
||||
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
|
||||
import com.google.samples.apps.nowinandroid.ui.NiaAppState
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.impl.interestsListDetailScreen
|
||||
|
||||
/**
|
||||
* Top-level navigation graph. Navigation is organized as explained at
|
||||
* https://d.android.com/jetpack/compose/nav-adaptive
|
||||
*
|
||||
* The navigation graph defined in this file defines the different top level routes. Navigation
|
||||
* within each route is handled using state and Back Handlers.
|
||||
*/
|
||||
@Composable
|
||||
fun NiaNavHost(
|
||||
appState: NiaAppState,
|
||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val navController = appState.navController
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = ForYouRoute,
|
||||
modifier = modifier,
|
||||
) {
|
||||
forYouSection(
|
||||
onTopicClick = navController::navigateToTopic,
|
||||
) {
|
||||
topicScreen(
|
||||
showBackButton = true,
|
||||
onBackClick = navController::popBackStack,
|
||||
onTopicClick = navController::navigateToTopic,
|
||||
)
|
||||
}
|
||||
bookmarksScreen(
|
||||
onTopicClick = navController::navigateToInterests,
|
||||
onShowSnackbar = onShowSnackbar,
|
||||
)
|
||||
searchScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
|
||||
onTopicClick = navController::navigateToInterests,
|
||||
)
|
||||
interestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.ui
|
||||
|
||||
import androidx.navigation3.runtime.EntryProviderBuilder
|
||||
import androidx.navigation3.runtime.entry
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStack
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen
|
||||
|
||||
val MockEntryProvider: Set<EntryProviderBuilder<NiaNavKey>.() -> Unit> =
|
||||
setOf(
|
||||
{
|
||||
entry<ForYouRoute> {
|
||||
ForYouScreen({})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val startKey = ForYouRoute
|
||||
|
||||
fun mockNiaBackStack() = NiaBackStack(startKey)
|
||||
@ -0,0 +1,17 @@
|
||||
#
|
||||
# Copyright 2025 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
sdk = 35
|
||||
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.google.samples.apps.nowinandroid.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidFeatureApiConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
apply(plugin = "nowinandroid.android.library")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
|
||||
|
||||
dependencies {
|
||||
"api"(project(":core:navigation"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
# :core:navigation module
|
||||
## Dependency graph
|
||||

|
||||
@ -1,8 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nowinandroid.jvm.library)
|
||||
alias(libs.plugins.nowinandroid.android.library)
|
||||
alias(libs.plugins.nowinandroid.hilt)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.samples.apps.nowinandroid.core.navigation"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.savedstate.compose)
|
||||
|
||||
testImplementation(libs.truth)
|
||||
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.androidx.test.ext)
|
||||
androidTestImplementation(libs.androidx.compose.ui.testManifest)
|
||||
androidTestImplementation(libs.androidx.lifecycle.viewModel.testing)
|
||||
androidTestImplementation(libs.truth)
|
||||
}
|
||||
|
||||
@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.navigation
|
||||
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.createSavedStateHandle
|
||||
import androidx.lifecycle.viewmodel.testing.ViewModelScenario
|
||||
import androidx.lifecycle.viewmodel.testing.viewModelScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NiaBackStackViewModelTest {
|
||||
|
||||
@get:Rule val rule = createComposeRule()
|
||||
|
||||
private val serializersModules = SerializersModule {
|
||||
polymorphic(NiaNavKey::class) {
|
||||
subclass(TestStartKey::class, TestStartKey.serializer())
|
||||
subclass(TestTopLevelKeyFirst::class, TestTopLevelKeyFirst.serializer())
|
||||
subclass(TestTopLevelKeySecond::class, TestTopLevelKeySecond.serializer())
|
||||
subclass(TestKeyFirst::class, TestKeyFirst.serializer())
|
||||
subclass(TestKeySecond::class, TestKeySecond.serializer())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel() = NiaBackStackViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
niaBackStack = NiaBackStack(TestStartKey),
|
||||
serializersModules = serializersModules,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testStartKeySaved() {
|
||||
rule.setContent {
|
||||
val viewModel = createViewModel()
|
||||
assertThat(viewModel.backStackMap).containsEntry(
|
||||
TestStartKey,
|
||||
mutableListOf(TestStartKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNonTopLevelKeySaved() {
|
||||
val viewModel = createViewModel()
|
||||
rule.setContent {
|
||||
val backStack = viewModel.niaBackStack
|
||||
|
||||
backStack.navigate(TestKeyFirst)
|
||||
}
|
||||
|
||||
assertThat(viewModel.backStackMap).containsEntry(
|
||||
TestStartKey,
|
||||
mutableListOf(TestStartKey, TestKeyFirst),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTopLevelKeySaved() {
|
||||
val viewModel = createViewModel()
|
||||
rule.setContent {
|
||||
val backStack = viewModel.niaBackStack
|
||||
|
||||
backStack.navigate(TestKeyFirst)
|
||||
backStack.navigate(TestTopLevelKeyFirst)
|
||||
}
|
||||
|
||||
assertThat(viewModel.backStackMap).containsExactly(
|
||||
TestStartKey,
|
||||
mutableListOf(TestStartKey, TestKeyFirst),
|
||||
TestTopLevelKeyFirst,
|
||||
mutableListOf(TestTopLevelKeyFirst),
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiStacksSaved() {
|
||||
val viewModel = createViewModel()
|
||||
rule.setContent {
|
||||
viewModel.niaBackStack.navigate(TestKeyFirst)
|
||||
viewModel.niaBackStack.navigate(TestTopLevelKeyFirst)
|
||||
viewModel.niaBackStack.navigate(TestKeySecond)
|
||||
}
|
||||
|
||||
assertThat(viewModel.backStackMap).containsExactly(
|
||||
TestStartKey,
|
||||
mutableListOf(TestStartKey, TestKeyFirst),
|
||||
TestTopLevelKeyFirst,
|
||||
mutableListOf(TestTopLevelKeyFirst, TestKeySecond),
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopSaved() {
|
||||
val viewModel = createViewModel()
|
||||
rule.setContent {
|
||||
val backStack = viewModel.niaBackStack
|
||||
|
||||
backStack.navigate(TestKeyFirst)
|
||||
assertThat(viewModel.backStackMap).containsExactly(
|
||||
TestStartKey,
|
||||
mutableListOf(TestStartKey, TestKeyFirst),
|
||||
)
|
||||
|
||||
backStack.popLast()
|
||||
assertThat(viewModel.backStackMap).containsExactly(
|
||||
TestStartKey,
|
||||
mutableListOf(TestStartKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestore() {
|
||||
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
|
||||
rule.setContent {
|
||||
scenario = viewModelScenario {
|
||||
NiaBackStackViewModel(
|
||||
savedStateHandle = createSavedStateHandle(),
|
||||
niaBackStack = NiaBackStack(TestStartKey),
|
||||
serializersModules = serializersModules,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
rule.runOnIdle {
|
||||
scenario.viewModel.niaBackStack.navigate(TestKeyFirst)
|
||||
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
scenario.recreate()
|
||||
|
||||
rule.runOnIdle {
|
||||
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestoreMultiStacks() {
|
||||
lateinit var scenario: ViewModelScenario<NiaBackStackViewModel>
|
||||
rule.setContent {
|
||||
scenario = viewModelScenario {
|
||||
NiaBackStackViewModel(
|
||||
savedStateHandle = createSavedStateHandle(),
|
||||
niaBackStack = NiaBackStack(TestStartKey),
|
||||
serializersModules = serializersModules,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
rule.runOnIdle {
|
||||
scenario.viewModel.niaBackStack.navigate(TestKeyFirst)
|
||||
scenario.viewModel.niaBackStack.navigate(TestTopLevelKeyFirst)
|
||||
scenario.viewModel.niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
scenario.recreate()
|
||||
|
||||
rule.runOnIdle {
|
||||
assertThat(scenario.viewModel.niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private object TestStartKey : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private object TestTopLevelKeyFirst : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private object TestTopLevelKeySecond : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private object TestKeyFirst : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private object TestKeySecond : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.navigation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.serialization.saved
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.savedstate.serialization.SavedStateConfiguration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.serializer
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NiaBackStackViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
val niaBackStack: NiaBackStack,
|
||||
serializersModules: SerializersModule,
|
||||
) : ViewModel() {
|
||||
|
||||
private val config = SavedStateConfiguration { serializersModule = serializersModules }
|
||||
|
||||
@VisibleForTesting
|
||||
internal var backStackMap by savedStateHandle.saved(
|
||||
serializer = getMapSerializer<NiaNavKey>(),
|
||||
configuration = config,
|
||||
) {
|
||||
linkedMapOf()
|
||||
}
|
||||
|
||||
init {
|
||||
if (backStackMap.isNotEmpty()) {
|
||||
// Restore backstack from saved state handle if not emtpy
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
niaBackStack.restore(
|
||||
backStackMap as LinkedHashMap<NiaNavKey, MutableList<NiaNavKey>>,
|
||||
)
|
||||
}
|
||||
|
||||
// Start observing changes to the backStack and save backStack whenever it updates
|
||||
viewModelScope.launch {
|
||||
snapshotFlow {
|
||||
niaBackStack.backStack.toList()
|
||||
backStackMap = niaBackStack.backStackMap
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T : NiaNavKey> getMapSerializer() = MapSerializer(serializer<T>(), serializer<List<T>>())
|
||||
@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.navigation
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NiaBackStackTest {
|
||||
|
||||
private lateinit var niaBackStack: NiaBackStack
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
niaBackStack = NiaBackStack(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStartKey() {
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigate() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevel() {
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateSingleTop() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNavigateTopLevelSingleTop() {
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubStack() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiStack() {
|
||||
// add to start stack
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
|
||||
// navigate to new top level
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// add to new stack
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// go back to start stack
|
||||
niaBackStack.navigate(TestStartKey)
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestore() {
|
||||
assertThat(niaBackStack.backStack).containsExactly(TestStartKey)
|
||||
|
||||
niaBackStack.restore(
|
||||
linkedMapOf(
|
||||
TestStartKey to mutableListOf(TestStartKey, TestKeyFirst),
|
||||
TestTopLevelKey to mutableListOf(TestTopLevelKey, TestKeySecond),
|
||||
),
|
||||
)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKey,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeySecond)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneNonTopLevel() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.popLast()
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPopOneTopLevel() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestTopLevelKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestTopLevelKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestTopLevelKey)
|
||||
|
||||
// remove TopLevel
|
||||
niaBackStack.popLast()
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestKeyFirst)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleNonTopLevel() {
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestKeyFirst,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.popLast(2)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun popMultipleTopLevel() {
|
||||
val testTopLevelKeyTwo = object : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
// second sub-stack
|
||||
niaBackStack.navigate(TestTopLevelKey)
|
||||
niaBackStack.navigate(TestKeyFirst)
|
||||
// third sub-stack
|
||||
niaBackStack.navigate(testTopLevelKeyTwo)
|
||||
niaBackStack.navigate(TestKeySecond)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
TestTopLevelKey,
|
||||
TestKeyFirst,
|
||||
testTopLevelKeyTwo,
|
||||
TestKeySecond,
|
||||
).inOrder()
|
||||
|
||||
niaBackStack.popLast(4)
|
||||
|
||||
assertThat(niaBackStack.backStack).containsExactly(
|
||||
TestStartKey,
|
||||
).inOrder()
|
||||
|
||||
assertThat(niaBackStack.currentKey).isEqualTo(TestStartKey)
|
||||
assertThat(niaBackStack.currentTopLevelKey).isEqualTo(TestStartKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun throwOnEmptyBackStack() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
niaBackStack.popLast(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object TestStartKey : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
private object TestTopLevelKey : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
private object TestKeyFirst : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
private object TestKeySecond : NiaNavKey {
|
||||
override val isTopLevel: Boolean
|
||||
get() = false
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
# :feature:bookmarks:api module
|
||||
## Dependency graph
|
||||

|
||||
@ -0,0 +1,3 @@
|
||||
# :feature:bookmarks:impl module
|
||||
## Dependency graph
|
||||

|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksRoute
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
||||
|
||||
/**
|
||||
* Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer
|
||||
*
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object BookmarksSerializerModule {
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideBookmarksPolymorphicModuleBuilder(): PolymorphicModuleBuilder<@JvmSuppressWildcards NiaNavKey>.() -> Unit = {
|
||||
subclass(BookmarksRoute::class, BookmarksRoute.serializer())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
# :feature:foryou:api module
|
||||
## Dependency graph
|
||||

|
||||
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.foryou.api.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaBackStackKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable data object ForYouRoute: NiaBackStackKey // route to ForYou screen
|
||||
|
||||
fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(route = ForYouRoute, navOptions)
|
||||
|
||||
/**
|
||||
* The ForYou section of the app. It can also display information about topics.
|
||||
* This should be supplied from a separate module.
|
||||
*
|
||||
* @param onTopicClick - Called when a topic is clicked, contains the ID of the topic
|
||||
* @param topicDestination - Destination for topic content
|
||||
*/
|
||||
fun NavGraphBuilder.forYouSection(
|
||||
onTopicClick: (String) -> Unit,
|
||||
topicDestination: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
// navigation<ForYouBaseRoute>(startDestination = ForYouRoute) {
|
||||
// composable<ForYouRoute>(
|
||||
// deepLinks = listOf(
|
||||
// navDeepLink {
|
||||
// /**
|
||||
// * This destination has a deep link that enables a specific news resource to be
|
||||
// * opened from a notification (@see SystemTrayNotifier for more). The news resource
|
||||
// * ID is sent in the URI rather than being modelled in the route type because it's
|
||||
// * transient data (stored in SavedStateHandle) that is cleared after the user has
|
||||
// * opened the news resource.
|
||||
// */
|
||||
// uriPattern = DEEP_LINK_URI_PATTERN
|
||||
// },
|
||||
// ),
|
||||
// ) {
|
||||
// com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen(onTopicClick)
|
||||
// }
|
||||
// topicDestination()
|
||||
// }
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.foryou.api.navigation
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object ForYouRoute : NiaNavKey { // route to ForYou screen
|
||||
override val isTopLevel: Boolean
|
||||
get() = true
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
# :feature:foryou:impl module
|
||||
## Dependency graph
|
||||

|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.navigation.NiaNavKey
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouRoute
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
||||
|
||||
/**
|
||||
* Provides the DSL to register the route's [kotlinx.serialization.KSerializer] as a polymorphic serializer
|
||||
*
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ForYouRouteSerializerModule {
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideForYouPolymorphicModuleBuilder(): PolymorphicModuleBuilder<@JvmSuppressWildcards NiaNavKey>.() -> Unit = {
|
||||
subclass(ForYouRoute::class, ForYouRoute.serializer())
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 122 KiB |
@ -0,0 +1,3 @@
|
||||
# :feature:interests:api module
|
||||
## Dependency graph
|
||||

|
||||