android / nowinandroid

A fully functional Android app built entirely with Kotlin and Jetpack Compose
Apache License 2.0
16.52k stars 2.97k forks source link

How to implement navigation type safety in NiaNavHost? #1613

Open padrecedano opened 2 days ago

padrecedano commented 2 days ago

According to this post, starting with version 2.8.0-alpha08 of Navigation objects can be used in navigation.

I'm trying to implement it, because I need to pass instances of an object called ItemUI instead of passing a simple value, but it doesn't work for me.

This is what I've been trying:

ForYouNavigation

fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(FOR_YOU_ROUTE, navOptions)

@ExperimentalMaterial3AdaptiveApi
fun NavGraphBuilder.forYouScreen(onTopicClick: (ItemUI) -> Unit) {
    composable(
        route = FOR_YOU_ROUTE,
    ) {
        ForYouRoute(onTopicClick = onTopicClick)
    }
}

NiaNavHost

@Composable
fun NiaNavHost(
    appState: NiaAppState,
    onShowSnackbar: suspend (String, String?) -> Boolean,
    modifier: Modifier = Modifier,
    startDestination: String = FOR_YOU_ROUTE,
    onReaderClick: () -> Unit

) {
    val navController = appState.navController
    NavHost(
        navController = navController,
        startDestination = startDestination,
        modifier = modifier,
    ) {
        forYouScreen(onTopicClick = navController::navigateToTodays)
        // ...
}

navigateToTodays

fun NavController.navigateToTodays(topicId: ItemUI? = null, navOptions: NavOptions? = null) {
    /*val route = if (topicId != null) {
        "${INTERESTS_ROUTE_BASE}?${TOPIC_ID_ARG}=$topicId"
    } else {
        INTERESTS_ROUTE_BASE
    }*/
    navigate(topicId, navOptions)
}

At this line

navigate(topicId, navOptions)

I have this error:

Type mismatch: inferred type is ItemUI? but TypeVariable(T) was expected

saeedishayan76 commented 2 days ago

In typeSafeNavigation (version 2.8.0) you can pass a route: T as argument, this class should be Serializable , you can pass item in this class like this:

@Serializable
object ScreenA

@Serializable
data class ScreenB(
    val user: User
)
@Serializable
data class User(
    val name: String,
    val family: String
)

Then you should create an custom type for this type like this :

object CustomNavType {
    val userType = object : NavType<User>(
        isNullableAllowed = false
    ) {
        override fun get(bundle: Bundle, key: String): User? {
            return Json.decodeFromString(bundle.getString(key) ?: return null)
        }

        override fun parseValue(value: String): User {
            return Json.decodeFromString(Uri.decode(value))
        }

        override fun serializeAsValue(value: User): String {
            return Uri.encode(Json.encodeToString(value))
        }

        override fun put(bundle: Bundle, key: String, value: User) {
            bundle.putString(key, Json.encodeToString(value))
        }

    }
}

Then you should use typeMap as argument for screenB like this:

composable<ScreenB>(
     typeMap = mapOf(
          typeOf<User>() to CustomNavType.userType
        )
    ) 

And now you can pass data from another screen like this :

   Column(
       modifier = Modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Center
         ) {
          Text("Home", modifier = Modifier
           .clickable {
             navController.navigate(ScreenB(
                User("Ali", "Saeedi"),
                 ), navOptions {
                  popUpTo(ScreenA) {
                   inclusive = true
                }
      })
    }
 }

In your way, class ItemUi must be annotated with Serialization. Also, if this class contains a custom variable, it must also be Serialized, then you can use :

navigate(ItemUi(...), navOptions)

For your issue:

Should add new composable to NiaNavHost, then from foryouScreen when onTopicCall , do a work like this

forYouScreen(
   onTopicClick = navController::navigateToTodays
        )

You should have this

composable<ItemUi> (
     typeMap = mapOf ( typeOf ... )  ->  if you have custom type
){ backStackEntry ->

   val data = backStackEntry.toRoute<ItemUI>()

   TodaysScreen(data, .....)
}

@padrecedano