Use SafeArgs navigation for Topic feature

Change-Id: Idf4386f10c780d3edc1f8aa11b428cb146e982c3
dt/nav-safe-args-android-dependency
Don Turner 8 months ago
parent b9935368e3
commit 6dc59e7016

@ -28,6 +28,10 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
pluginManager.apply { pluginManager.apply {
apply("nowinandroid.android.library") apply("nowinandroid.android.library")
apply("nowinandroid.android.hilt") apply("nowinandroid.android.hilt")
// Serialization is used for type-safe navigation.
// TODO: Use the plugin ID from the version catalog when
// https://github.com/gradle/gradle/issues/15383# is resolved
apply("org.jetbrains.kotlin.plugin.serialization")
} }
extensions.configure<LibraryExtension> { extensions.configure<LibraryExtension> {
defaultConfig { defaultConfig {
@ -45,7 +49,9 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
add("implementation", libs.findLibrary("androidx.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) add("implementation", libs.findLibrary("androidx.tracing.ktx").get())
add("implementation", libs.findLibrary("kotlinx.serialization.json").get())
add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get())
} }

@ -28,7 +28,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult import com.google.samples.apps.nowinandroid.core.result.asResult
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicArgs import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -47,12 +47,14 @@ class TopicViewModel @Inject constructor(
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
) : ViewModel() { ) : ViewModel() {
private val topicArgs: TopicArgs = TopicArgs(savedStateHandle) // TODO: Remove when alpha08 is released
private val topicDestination = TopicDestination(savedStateHandle["id"]!!)
//private val topicDestination : TopicDestination = savedStateHandle.toRoute()
val topicId = topicArgs.topicId val topicId = topicDestination.id
val topicUiState: StateFlow<TopicUiState> = topicUiState( val topicUiState: StateFlow<TopicUiState> = topicUiState(
topicId = topicArgs.topicId, topicId = topicDestination.id,
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
topicsRepository = topicsRepository, topicsRepository = topicsRepository,
) )
@ -63,7 +65,7 @@ class TopicViewModel @Inject constructor(
) )
val newsUiState: StateFlow<NewsUiState> = newsUiState( val newsUiState: StateFlow<NewsUiState> = newsUiState(
topicId = topicArgs.topicId, topicId = topicDestination.id,
userDataRepository = userDataRepository, userDataRepository = userDataRepository,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
) )
@ -75,7 +77,7 @@ class TopicViewModel @Inject constructor(
fun followTopicToggle(followed: Boolean) { fun followTopicToggle(followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
userDataRepository.setTopicIdFollowed(topicArgs.topicId, followed) userDataRepository.setTopicIdFollowed(topicDestination.id, followed)
} }
} }

@ -17,33 +17,23 @@
package com.google.samples.apps.nowinandroid.feature.topic.navigation package com.google.samples.apps.nowinandroid.feature.topic.navigation
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute
import java.net.URLDecoder import kotlinx.serialization.Serializable
import java.net.URLEncoder
import kotlin.text.Charsets.UTF_8
private val URL_CHARACTER_ENCODING = UTF_8.name()
// TODO: Remove
@VisibleForTesting @VisibleForTesting
internal const val TOPIC_ID_ARG = "topicId" internal const val TOPIC_ID_ARG = "topicId"
const val TOPIC_ROUTE = "topic_route" const val TOPIC_ROUTE = "topic_route"
internal class TopicArgs(val topicId: String) { @Serializable
constructor(savedStateHandle: SavedStateHandle) : data class TopicDestination(val id: String)
this(URLDecoder.decode(checkNotNull(savedStateHandle[TOPIC_ID_ARG]), URL_CHARACTER_ENCODING))
}
fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) { fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) {
val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) navigate(TopicDestination(topicId)) {
val newRoute = "$TOPIC_ROUTE/$encodedId"
navigate(newRoute) {
navOptions() navOptions()
} }
} }
@ -53,12 +43,7 @@ fun NavGraphBuilder.topicScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
) { ) {
composable( composable<TopicDestination> {
route = "topic_route/{$TOPIC_ID_ARG}",
arguments = listOf(
navArgument(TOPIC_ID_ARG) { type = NavType.StringType },
),
) {
TopicRoute( TopicRoute(
showBackButton = showBackButton, showBackButton = showBackButton,
onBackClick = onBackClick, onBackClick = onBackClick,
@ -66,3 +51,4 @@ fun NavGraphBuilder.topicScreen(
) )
} }
} }

@ -20,7 +20,8 @@ androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.7.0" androidxLifecycle = "2.7.0"
androidxMacroBenchmark = "1.2.2" androidxMacroBenchmark = "1.2.2"
androidxMetrics = "1.0.0-alpha04" androidxMetrics = "1.0.0-alpha04"
androidxNavigation = "2.7.4" #androidxNavigation = "2.8.0-SNAPSHOT"
androidxNavigation = "2.8.0-alpha07"
androidxProfileinstaller = "1.3.1" androidxProfileinstaller = "1.3.1"
androidxTestCore = "1.5.0" androidxTestCore = "1.5.0"
androidxTestExt = "1.1.5" androidxTestExt = "1.1.5"

Loading…
Cancel
Save