@ -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 {
|
plugins {
|
||||||
alias(libs.plugins.nowinandroid.jvm.library)
|
alias(libs.plugins.nowinandroid.android.library)
|
||||||
alias(libs.plugins.nowinandroid.hilt)
|
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 {
|
dependencies {
|
||||||
api(libs.androidx.navigation3.runtime)
|
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
|
||||||
|

|
||||||