Gopikrishnan S
Gopikrishnan S

Reputation: 65

How to Set Default Values for Navigation Arguments in Jetpack Compose's Type-Safe Navigation?

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

Answers (2)

Tejas Soni
Tejas Soni

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

Adnan Habib
Adnan Habib

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

Related Questions