Reputation: 65
I recently started using the new type-safe navigation feature in Jetpack Compose. Previously, I could pass default values for navigation arguments like this:
class WalletGiftDetailLandingArgs(
private val savedStateHandle: SavedStateHandle,
) {
val giftRefId: String
get() = savedStateHandle[KEY_GIFT_REF_ID] ?: ""
val isFromRedemptionReminderNotification: Boolean
get() = savedStateHandle[KEY_IS_FROM_REDEMPTION_REMINDER_NOTIFICATION] ?: false
}
object WalletGiftDetailDestination : NavigableDestination(
route = "$ROUTE/{$KEY_GIFT_TYPE}/{$KEY_GIFT_REF_ID}/{$KEY_IS_FROM_REDEMPTION_REMINDER_NOTIFICATION}",
)
fun NavGraphBuilder.walletGiftDetailScreen(
giftRefId: String = "",
isFromRedemptionReminderNotification: Boolean,
onClickBack: () -> Unit,
onRemoveFromWalletSuccess: () -> Unit,
onRedirectToReceivedGiftsScreen: () -> Unit,
) {
composable(
route = WalletGiftDetailDestination.route,
arguments = listOf(
navArgument(KEY_GIFT_REF_ID) {
type = NavType.StringType
defaultValue = giftRefId
},
navArgument(KEY_IS_FROM_REDEMPTION_REMINDER_NOTIFICATION) {
type = NavType.BoolType
defaultValue = isFromRedemptionReminderNotification
}
)
) {
WalletGiftDetailScreenRoute(
onClickBack = onClickBack,
onRemoveFromWalletSuccess = onRemoveFromWalletSuccess,
onRedirectToReceivedGiftsScreen = onRedirectToReceivedGiftsScreen
)
}
}
In the ViewModel, I could retrieve the values from the SavedStateHandle safely.
With the new type-safe navigation, I tried this:
@Serializable
class WalletGiftDetailLandingArgs(
val giftRefId: String,
val isFromRedemptionReminderNotification: Boolean,
)
fun NavGraphBuilder.walletGiftDetailScreen(
giftRefId: String = "",
isFromRedemptionReminderNotification: Boolean,
onClickBack: () -> Unit,
onRemoveFromWalletSuccess: () -> Unit,
onRedirectToReceivedGiftsScreen: () -> Unit,
) {
composable<WalletGiftDetailLandingArgs> {
WalletGiftDetailScreenRoute(
onClickBack = onClickBack,
onRemoveFromWalletSuccess = onRemoveFromWalletSuccess,
onRedirectToReceivedGiftsScreen = onRedirectToReceivedGiftsScreen
)
}
}
PROBLEM: However, I can’t figure out how to pass a default value for the arguments in the new type-safe navigation setup while the old compose navigations with String routes allowed this.
Additional Difficulty: If I try to handle default values manually by passing them directly to the composable, I encounter another issue: screen recomposition. Each recomposition calls the function again, which can cause the ViewModel to re-run logic (such as fetching or processing values passed to it). This is inefficient and can lead to unintended side effects.
Is there a proper way to define default arguments in the type-safe navigation system to avoid these issues, or is there a recommended workaround?
Any guidance would be greatly appreciated!
Any guidance would be appreciated!
Upvotes: 0
Views: 124
Reputation: 593
In the new type-safe navigation system in Jetpack Compose, you can handle default values for navigation arguments by defining them in the NavType
and using the defaultValue
parameter. However, since you are using a @Serializable
class for your arguments, you need to handle default values within the class itself and ensure that the navigation system respects these defaults.
Here's how you can achieve this:
First, update your WalletGiftDetailLandingArgs
class to include default values:
@Serializable
class WalletGiftDetailLandingArgs(
val giftRefId: String = "",
val isFromRedemptionReminderNotification: Boolean = false,
)
Next, create a custom NavType
to handle the serialization and deserialization of WalletGiftDetailLandingArgs
:
import androidx.navigation.NavType
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class WalletGiftDetailLandingArgsType : NavType<WalletGiftDetailLandingArgs>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): WalletGiftDetailLandingArgs? {
return bundle.getString(key)?.let { Json.decodeFromString(it) }
}
override fun parseValue(value: String): WalletGiftDetailLandingArgs {
return Json.decodeFromString(value)
}
override fun put(bundle: Bundle, key: String, value: WalletGiftDetailLandingArgs) {
bundle.putString(key, Json.encodeToString(value))
}
}
Then, update your NavGraphBuilder
to use this custom NavType
:
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
fun NavGraphBuilder.walletGiftDetailScreen(
giftRefId: String = "",
isFromRedemptionReminderNotification: Boolean = false,
onClickBack: () -> Unit,
onRemoveFromWalletSuccess: () -> Unit,
onRedirectToReceivedGiftsScreen: () -> Unit,
) {
composable(
route = WalletGiftDetailDestination.route,
arguments = listOf(
navArgument("walletGiftDetailLandingArgs") {
type = WalletGiftDetailLandingArgsType()
defaultValue = WalletGiftDetailLandingArgs(
giftRefId = giftRefId,
isFromRedemptionReminderNotification = isFromRedemptionReminderNotification
)
}
)
) { backStackEntry ->
val args = backStackEntry.arguments?.getParcelable<WalletGiftDetailLandingArgs>("walletGiftDetailLandingArgs")
WalletGiftDetailScreenRoute(
onClickBack = onClickBack,
onRemoveFromWalletSuccess = onRemoveFromWalletSuccess,
onRedirectToReceivedGiftsScreen = onRedirectToReceivedGiftsScreen
)
}
}
This approach ensures that default values are respected and avoids unnecessary recompositions. The custom NavType
handles the serialization and deserialization of your arguments, allowing you to pass complex objects with default values through the navigation system.
Upvotes: 0
Reputation: 1217
I've added comments to the code for better understanding. Let me know if you have any questions.
// Destination Route
@Serializable
data class WalletGiftDetailDestination(
// argument
val giftRefId: String = "", // default value
// argument
val isFromRedemptionReminderNotification: Boolean = false, // default value
)
fun NavGraphBuilder.walletGiftDetailScreen(
// don't pass arguments here
// giftRefId: String = "",
// isFromRedemptionReminderNotification: Boolean,
onClickBack: () -> Unit,
onRemoveFromWalletSuccess: () -> Unit,
onRedirectToReceivedGiftsScreen: () -> Unit,
) {
composable<WalletGiftDetailDestination> { backStackEntry ->
// get arguments from backStackEntry if you want to pass them to screen composable instead of getting them in viewModel
val destination: WalletGiftDetailDestination = backStackEntry.toRoute()
WalletGiftDetailScreenRoute(
// remove giftRefId and isFromRedemptionReminderNotification from WalletGiftDetailScreenRoute
// if you want to get them in viewModel
giftRefId = destination.giftRefId,
isFromRedemptionReminderNotification = destination.isFromRedemptionReminderNotification,
onClickBack = onClickBack,
onRemoveFromWalletSuccess = onRemoveFromWalletSuccess,
onRedirectToReceivedGiftsScreen = onRedirectToReceivedGiftsScreen
)
}
}
/////////////////////////////////////////
// can be used to get arguments in viewModel but seems boilerplate code to me with type-safe navigation
class WalletGiftDetailLandingArgs(
private val savedStateHandle: SavedStateHandle,
) {
val giftRefId: String
get() = savedStateHandle.toRoute<WalletGiftDetailDestination>().giftRefId
val isFromRedemptionReminderNotification: Boolean
get() = savedStateHandle.toRoute<WalletGiftDetailDestination>().isFromRedemptionReminderNotification
}
class WalletGiftDetailViewModel(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
// get arguments from savedStateHandle in viewModel using WalletGiftDetailLandingArgs helper class
private val args = WalletGiftDetailLandingArgs(savedStateHandle)
private val betterArgs = savedStateHandle.toRoute<WalletGiftDetailDestination>()
init {
println(args.giftRefId)
println(args.isFromRedemptionReminderNotification)
println(betterArgs.giftRefId)
println(betterArgs.isFromRedemptionReminderNotification)
}
}
/////////////////////////////////////////
navController.navigate(
WalletGiftDetailDestination(
// pass arguments here if needed otherwise default values will be used
)
)
Upvotes: 1