Jetpack Compose Navigation default value not working for boolean - android

Trying to send a default value of boolean type through to one of my screens using compose navigation, but the default value never gets applied - can anyone see what the issue might be here?
route declaration ->
sealed class Screens(
val coreRoute: String,
val routeWithArgs: String? = null
) {
fun navigateWithArgs(
navController: NavController,
coreRoute: String,
args: List<Any>
) {
navController.navigate("$coreRoute/${args.joinToString("/")}")
}
object Splash : Screens(coreRoute = "splash")
object Establish : Screens(
coreRoute = "establish",
routeWithArgs = "establish/{isChanging}"
)
}
Screen setup ->
#OptIn(ExperimentalAnimationApi::class)
fun NavGraphBuilder.addEstablishScreen(
navController: NavController
) {
Screens.Establish.routeWithArgs?.let { route ->
composable(
route = route,
arguments = listOf(navArgument("isChanging") {
defaultValue = false
type = NavType.BoolType
})
) {
EstablishComposable(
navigateToHome = {
navController.navigate(Screens.Home.coreRoute)
},
navigateBack = {
navController.navigateUp()
}
)
}
}
}
Navigating to screen ->
#OptIn(ExperimentalAnimationApi::class)
fun NavGraphBuilder.addSplashScreen(
navController: NavController
) {
composable(route = Screens.Splash.coreRoute) {
SplashComposable(
navigateToEstablish = {
Screens.Establish.routeWithArgs?.let { route -> navController.navigate(route) }
},
navigateToHome = {
navController.navigate(Screens.Home.coreRoute)
}
)
}
}

Related

Compose app doesn't load api data after launch

I'm creating a Pokémon app with Jetpack Compose. I'm testing it with two smartphones: Xioami Mi 11T Pro (Android 12) and Xiaomi Mi 8 Lite (Android 10).
Well, when I launch the app in the Mi 8 Lite, it starts correctly, the pokemon list loads perfectly.
But when I launch the app with the Mi 11 T Pro, it doesn't load, nothing shows. I discovered two things:
If I open the Layout Inspector it loads inmediately, without doing anything more...
When the screen is empty (just after launch, before it loads), If I click 1-2 times on the screen it starts to send the request and loads correctly.
Why is this happening?
I attach my ViewModel and my MainActivity.
PokemonListViewModel.kt
#HiltViewModel
class PokemonListViewModel #Inject constructor(
private val repository: PokemonRepositoryImpl
) : ViewModel() {
private var currentPage = 0
var pokemonList = mutableStateOf<List<PokedexListEntry>>(listOf())
var loadError = mutableStateOf("")
var isLoading = mutableStateOf(false)
var endReached = mutableStateOf(false)
private var cachedPokemonList = listOf<PokedexListEntry>()
private var isSearchStarting = true
var isSearching = mutableStateOf(false)
init {
loadPokemonList()
}
// TODO: Search online, not only already loaded pokémon
fun searchPokemonList(query: String) {
val listToSearch = if (isSearchStarting) {
pokemonList.value
} else {
// If we typed at least one character
cachedPokemonList
}
viewModelScope.launch(Dispatchers.Default) {
if (query.isEmpty()) {
pokemonList.value = cachedPokemonList
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
// Search by name or pokédex number
it.pokemonName.contains(query.trim(), true) ||
it.number.toString() == query.trim()
}
if (isSearchStarting) {
cachedPokemonList = pokemonList.value
isSearchStarting = false
}
// Update entries with the results
pokemonList.value = results
isSearching.value = true
}
}
fun loadPokemonList() {
viewModelScope.launch {
isLoading.value = true
val result = repository.getPokemonList(PAGE_SIZE, currentPage * PAGE_SIZE)
when (result) {
is Resource.Success -> {
endReached.value = currentPage * PAGE_SIZE >= result.data!!.count
val pokedexEntries = result.data.results.mapIndexed { index, entry ->
val number = getPokedexNumber(entry)
val url = getImageUrl(number)
PokedexListEntry(
entry.name.replaceFirstChar(Char::titlecase),
url,
number.toInt()
)
}
currentPage++
loadError.value = ""
isLoading.value = false
pokemonList.value += pokedexEntries
}
is Resource.Error -> {
loadError.value = result.message!!
isLoading.value = false
}
is Resource.Loading -> {
isLoading.value = true
}
}
}
}
private fun getImageUrl(number: String): String {
return "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png"
}
private fun getPokedexNumber(entry: Result): String {
return if (entry.url.endsWith("/")) {
entry.url.dropLast(1).takeLastWhile { it.isDigit() }
} else {
entry.url.takeLastWhile { it.isDigit() }
}
}
}
MainActivity.kt
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val argPokemonName = "pokemonName"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposePokedexTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "pokemon_list_screen") {
composable("pokemon_list_screen") {
PokemonListScreen(navController = navController)
}
composable(
"pokemon_detail_screen/{$argPokemonName}",
arguments = listOf(
navArgument(argPokemonName) {
type = NavType.StringType
}
)
) {
val pokemonName = remember {
it.arguments?.getString(argPokemonName)
}
PokemonDetailScreen(
pokemonName = pokemonName?.lowercase(Locale.ROOT) ?: "",
navController = navController
)
}
}
}
}
}
}
If someone knows why it doesn't load... I suspect that maybe init { } or Hilt injection are doing something that makes init doesn't start or something.
Thanks for your time and help!
Well, it seems is a Xiaomi reported Bug that google won't fix, you can see it here:
https://issuetracker.google.com/issues/227926002
It worked for me adding a little delay before set content and it seems to be working:
lifecycleScope.launch {
delay(300)
setContent {
JetpackComposePokedexTheme {
...
}
}
}
Also you can see: compose NavHost Start the white Screen

Navigation destination that matches request NavDeepLinkRequest cannot be found in the navigation graph NavGraph

I have a NavGraph that looks like this:
#Composable
fun NavGraph (
navController: NavHostController
) {
NavHost(
navController = navController,
startDestination = "Products"
) {
composable(
route = "Products"
) {
ProductsScreen(
navController = navController
)
}
composable(
route = "Product Details",
arguments = listOf(
navArgument("product") {
type = NavType.SerializableType(Product::class.java)
}
)
) {
val product = navController.previousBackStackEntry?.arguments?.getSerializable("product") as Product
ProductDetailsScreen(
navController = navController,
product = product
)
}
}
}
Inside the ProductDetailsScreen, I want on product click to navigate further to details screen passing a Product object:
LazyColumn {
items(
items = products
) { product ->
ProductCard(
product = product,
onProductClick = {
navController.currentBackStackEntry?.arguments?.putSerializable("product", product)
navController.navigate("Product Details")
}
)
}
}
The products are perfectly displayed but when I click on a product, the app crashes with this error:
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/Product Details } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0xb543811) route=Products}
Can anyone help?
P.S. I also followed this answer but no luck :(
Make sure you have all the argument names and route names correctly setup and avoid spaces, use underscores when you want to break words
My Mistake
The argument name in the route definition and while supplying in the navArgument block should be same
composable(
"pokemon_detail_screen/{dominantColor}/{pokemonName}", //Notice over here
arguments = listOf(
navArgument("dominantColor") {
type = NavType.IntType
},
navArgument("pokemon_name") { // Notice over here
type = NavType.StringType
}
)
)
Took me quite some time to figure it out so be very careful
It is my sample
#ExperimentalPagerApi
#Composable
fun WallNavigation(nameFolder: String, paths: List<String>) {
val navController = rememberNavController()
val model: ImageViewModel = viewModel()
model.setFolder(nameFolder)
model.setFileNames(paths)
NavHost(navController = navController, startDestination = Screen.MainScreen.route) {
composable(Screen.MainScreen.route) {
MainScreen(navController = navController, model)
}
composable(route = Screen.GalleryScreen.route + "/{position}", arguments = listOf(navArgument(name = "position") {
type = NavType.IntType
defaultValue = -1
nullable = false
})) { entity ->
GalleryScreen(navController = navController, position = entity.arguments?.getInt("position") ?: -1, imageViewModel = model)
}
composable(route = Screen.CropScreen.route) {
CropScreen(navController = navController, imageViewModel = model)
}
}
}
sealed class Screen(val route : String) {
object MainScreen : Screen("main_screen")
object GalleryScreen : Screen("gallery_screen")
object CropScreen : Screen("crop_screen")
fun withArgs(vararg args: String):String {
return buildString {
append(route)
args.forEach { arg ->
append("/$arg")
}
}
}
}
This answer explains really good how to pass a parcelable argument with compose navigation.
The error message you describe above that the navigation destination that matches request NavDeepLinkRequest cannot be found in the navigation graph NavGraph can also appear when you want to hand over an optional argument and forget to make it nullable.
arguments: List<NamedNavArgument> = listOf(
navArgument("product") {
type = Product.NavigationType //check out the linked answer to know how to write the Product.NavigationType
nullable = true
}
)

How to use the argument that I passed from another screen using compose?

I want to use accessToken in the AccountListScreen which I pass from AuthenticationScreen
Here is code of my MainActivity:
composable(
route = Screen.AuthenticationScreen.route
) {
AuthenticationScreen(navController)
}
composable(
route = Screen.AccountListScreen.route + "/{accessToken}"
) {
AccountListScreen(navController)
}
So I'm trying to pass a parameter from my AuthenticationScreen:
#Composable
fun AuthenticationScreen(
navController: NavController,
viewModel: AuthenticationViewModel = hiltViewModel()
) {
val authorizeUrl: String by viewModel.authorizeUrl.observeAsState("")
WebPageScreen(urlToRender = authorizeUrl, viewModel = viewModel)
val state = viewModel.state.value
if (state.accessToken.isNotEmpty()) {
navController.navigate(Screen.AccountListScreen.route + "/${state.accessToken}")
}
}
And I don't understand how I can now get my parameter in AccountListScreen:
#Composable
fun AccountListScreen(
navController: NavController,
viewModel: AccountListViewModel = hiltViewModel()
) {
UPDATE
I try to use the #Alpha 1 solution and get the following error
maybe I'm doing something wrong
But I have a dependency connected:
implementation "androidx.navigation:navigation-compose:2.4.0-alpha10"
UPDATE 2
If you're using a NavHost, a way could be defining the composable like this in your MainActivity
object composablesData {
const val ACCESS_TOKEN = "ACCESS_TOKEN"
}
...
composable(
route = Screen.AccountListScreen.route + "/{$ACCESS_TOKEN}"
) {
val arguments = requireNotNull(it.arguments)
val token = arguments.getString(ACCESS_TOKEN) //set the correct type
AccountListScreen(navController,token)
}
And the just receive the parameter in your composable
#Composable
fun AccountListScreen(
navController: NavController,
token: String,
viewModel: AccountListViewModel = hiltViewModel()
) {
Check Navigate with arguments for more details and examples
You can specify the argument for a destination by passing a list of navArgument to your screen. refer the code below:
composable(
route = Screen.AccountListScreen.route + "/{accessToken}",
arguments = listOf(
navArgument("accessToken") {
type = NavType.StringType
}
)
) { entry ->
//reading the access token
val accessToken = entry.arguments?.getString("accessToken") ?: ""
AccountListScreen(navController)
}

My view recompose itself muitple times after changing state

i am working on compose project. I have simple login page. After i click login button, loginState is set in viewmodel. The problem is when i set loginState after service call, my composable recomposed itself multiple times. Thus, navcontroller navigates multiple times. I don't understand the issue. Thanks for helping.
My composable :
#Composable
fun LoginScreen(
navController: NavController,
viewModel: LoginViewModel = hiltViewModel()
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly
) {
val email by viewModel.email
val password by viewModel.password
val enabled by viewModel.enabled
if (viewModel.loginState.value) {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
}
LoginHeader()
LoginForm(
email = email,
password = password,
onEmailChange = { viewModel.onEmailChange(it) },
onPasswordChange = { viewModel.onPasswordChange(it) }
)
LoginFooter(
enabled,
onLoginClick = {
viewModel.login()
},
onRegisterClick = {
navController.navigate(Screen.RegisterScreen.route)
}
)
}
ViewModel Class:
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginRepository: LoginRepository,
) : BaseViewModel() {
val email = mutableStateOf(EMPTY)
val password = mutableStateOf(EMPTY)
val enabled = mutableStateOf(false)
val loginState = mutableStateOf(false)
fun onEmailChange(email: String) {
this.email.value = email
checkIfInputsValid()
}
fun onPasswordChange(password: String) {
this.password.value = password
checkIfInputsValid()
}
private fun checkIfInputsValid() {
enabled.value =
Validator.isEmailValid(email.value) && Validator.isPasswordValid(password.value)
}
fun login() = viewModelScope.launch {
val response = loginRepository.login(LoginRequest(email.value, password.value))
loginRepository.saveSession(response)
loginState.value = response.success ?: false
}
}
You should not cause side effects or change the state directly from the composable builder, because this will be performed on each recomposition.
Instead you can use side effects. In your case, LaunchedEffect can be used.
if (viewModel.loginState.value) {
LaunchedEffect(Unit) {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
}
}
But I think that much better solution is not to listen for change of loginState, but to make login a suspend function, wait it to finish and then perform navigation. You can get a coroutine scope which will be bind to your composable with rememberCoroutineScope. It can look like this:
suspend fun login() : Boolean {
val response = loginRepository.login(LoginRequest(email.value, password.value))
loginRepository.saveSession(response)
return response.success ?: false
}
Also check out Google engineer thoughts about why you shouldn't pass NavController as a parameter in this answer (As per the Testing guide for Navigation Compose ...)
So your view after updates will look like:
#Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel(),
onLoggedIn: () -> Unit,
onRegister: () -> Unit,
) {
// ...
val scope = rememberCoroutineScope()
LoginFooter(
enabled,
onLoginClick = {
scope.launch {
if (viewModel.login()) {
onLoggedIn()
}
}
},
onRegisterClick = onRegister
)
// ...
}
And your navigation route:
composable(route = "login") {
LoginScreen(
onLoggedIn = {
navController.navigate(Screen.HomeScreen.route) {
popUpTo(Screen.LoginScreen.route) {
inclusive = true
}
}
},
onRegister = {
navController.navigate(Screen.RegisterScreen.route)
}
)
}

Pass Parcelable argument with compose navigation

I want to pass a parcelable object (BluetoothDevice) to a composable using compose navigation.
Passing primitive types is easy:
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
navController.navigate("profile/user1234")
But I can't pass a parcelable object in the route unless I can serialize it to a string.
composable(
"deviceDetails/{device}",
arguments = listOf(navArgument("device") { type = NavType.ParcelableType(BluetoothDevice::class.java) })
) {...}
val device: BluetoothDevice = ...
navController.navigate("deviceDetails/$device")
The code above obviously doesn't work because it just implicitly calls toString().
Is there a way to either serialize a Parcelable to a String so I can pass it in the route or pass the navigation argument as an object with a function other than navigate(route: String)?
Warning:
Ian Lake is an Android Developer Advocate and he says in this answer that pass complex data structures is an anti-pattern (referring the documentation). He works on this library, so he has authority on this. Use the approach below by your own.
Edit: Updated to Compose Navigation 2.4.0-beta07
Seems like previous solution is not supported anymore. Now you need to create a custom NavType.
Let's say you have a class like:
#Parcelize
data class Device(val id: String, val name: String) : Parcelable
Then you need to define a NavType
class AssetParamType : NavType<Device>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): Device? {
return bundle.getParcelable(key)
}
override fun parseValue(value: String): Device {
return Gson().fromJson(value, Device::class.java)
}
override fun put(bundle: Bundle, key: String, value: Device) {
bundle.putParcelable(key, value)
}
}
Notice that I'm using Gson to convert the object to a JSON string. But you can use the conversor that you prefer...
Then declare your composable like this:
NavHost(...) {
composable("home") {
Home(
onClick = {
val device = Device("1", "My device")
val json = Uri.encode(Gson().toJson(device))
navController.navigate("details/$json")
}
)
}
composable(
"details/{device}",
arguments = listOf(
navArgument("device") {
type = AssetParamType()
}
)
) {
val device = it.arguments?.getParcelable<Device>("device")
Details(device)
}
}
Original answer
Basically you can do the following:
// In the source screen...
navController.currentBackStackEntry?.arguments =
Bundle().apply {
putParcelable("bt_device", device)
}
navController.navigate("deviceDetails")
And in the details screen...
val device = navController.previousBackStackEntry
?.arguments?.getParcelable<BluetoothDevice>("bt_device")
I've written a small extension for the NavController.
import android.os.Bundle
import androidx.core.net.toUri
import androidx.navigation.*
fun NavController.navigate(
route: String,
args: Bundle,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
val routeLink = NavDeepLinkRequest
.Builder
.fromUri(NavDestination.createRoute(route).toUri())
.build()
val deepLinkMatch = graph.matchDeepLink(routeLink)
if (deepLinkMatch != null) {
val destination = deepLinkMatch.destination
val id = destination.id
navigate(id, args, navOptions, navigatorExtras)
} else {
navigate(route, navOptions, navigatorExtras)
}
}
As you can check there are at least 16 functions "navigate" with different parameters, so it's just a converter for use
public open fun navigate(#IdRes resId: Int, args: Bundle?)
So using this extension you can use Compose Navigation without these terrible deep link parameters for arguments at routes.
Here's my version of using the BackStackEntry
Usage:
composable("your_route") { entry ->
AwesomeScreen(entry.requiredArg("your_arg_key"))
}
navController.navigate("your_route", "your_arg_key" to yourArg)
Extensions:
fun NavController.navigate(route: String, vararg args: Pair<String, Parcelable>) {
navigate(route)
requireNotNull(currentBackStackEntry?.arguments).apply {
args.forEach { (key: String, arg: Parcelable) ->
putParcelable(key, arg)
}
}
}
inline fun <reified T : Parcelable> NavBackStackEntry.requiredArg(key: String): T {
return requireNotNull(arguments) { "arguments bundle is null" }.run {
requireNotNull(getParcelable(key)) { "argument for $key is null" }
}
}
Here is another solution that works also by adding the Parcelable to the correct NavBackStackEntry, NOT the previous entry. The idea is first to call navController.navigate, then add the argument to the last NavBackStackEntry.arguments in the NavController.backQueue. Be mindful that this does use another library group restricted API (annotated with RestrictTo(LIBRARY_GROUP)), so could potentially break. Solutions posted by some others use the restricted NavBackStackEntry.arguments, however NavController.backQueue is also restricted.
Here are some extensions for the NavController for navigating and NavBackStackEntry for retrieving the arguments within the route composable:
fun NavController.navigate(
route: String,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
args: List<Pair<String, Parcelable>>? = null,
) {
if (args == null || args.isEmpty()) {
navigate(route, navOptions, navigatorExtras)
return
}
navigate(route, navOptions, navigatorExtras)
val addedEntry: NavBackStackEntry = backQueue.last()
val argumentBundle: Bundle = addedEntry.arguments ?: Bundle().also {
addedEntry.arguments = it
}
args.forEach { (key, arg) ->
argumentBundle.putParcelable(key, arg)
}
}
inline fun <reified T : Parcelable> NavController.navigate(
route: String,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
arg: T? = null,
) {
if (arg == null) {
navigate(route, navOptions, navigatorExtras)
return
}
navigate(
route = route,
navOptions = navOptions,
navigatorExtras = navigatorExtras,
args = listOf(T::class.qualifiedName!! to arg),
)
}
fun NavBackStackEntry.requiredArguments(): Bundle = arguments ?: throw IllegalStateException("Arguments were expected, but none were provided!")
#Composable
inline fun <reified T : Parcelable> NavBackStackEntry.rememberRequiredArgument(
key: String = T::class.qualifiedName!!,
): T = remember {
requiredArguments().getParcelable<T>(key) ?: throw IllegalStateException("Expected argument with key: $key of type: ${T::class.qualifiedName!!}")
}
#Composable
inline fun <reified T : Parcelable> NavBackStackEntry.rememberArgument(
key: String = T::class.qualifiedName!!,
): T? = remember {
arguments?.getParcelable(key)
}
To navigate with a single argument, you can now do this in the scope of a NavGraphBuilder:
composable(route = "screen_1") {
Button(
onClick = {
navController.navigate(
route = "screen_2",
arg = MyParcelableArgument(whatever = "whatever"),
)
}
) {
Text("goto screen 2")
}
}
composable(route = "screen_2") { entry ->
val arg: MyParcelableArgument = entry.rememberRequiredArgument()
// TODO: do something with arg
}
Or if you want to pass multiple arguments of the same type:
composable(route = "screen_1") {
Button(
onClick = {
navController.navigate(
route = "screen_2",
args = listOf(
"arg_1" to MyParcelableArgument(whatever = "whatever"),
"arg_2" to MyParcelableArgument(whatever = "whatever"),
),
)
}
) {
Text("goto screen 2")
}
}
composable(route = "screen_2") { entry ->
val arg1: MyParcelableArgument = entry.rememberRequiredArgument(key = "arg_1")
val arg2: MyParcelableArgument = entry.rememberRequiredArgument(key = "arg_2")
// TODO: do something with args
}
The key benefit of this approach is that similar to the answer that uses Moshi to serialise the argument, it will work when popUpTo is used in the navOptions, but will also be more efficient as no JSON serialisation is involved.
This will of course not work with deep links, but it will survive process or activity recreation. For cases where you need to support deep links or even just optional arguments to navigation routes, you can use the entry.rememberArgument extension. Unlike entry.rememberRequiredArgument, it will return null instead of throwing an IllegalStateException.
The backStackEntry solution given by #nglauber will not work if we pop up (popUpTo(...)) back stacks on navigate(...).
So here is another solution. We can pass the object by converting it to a JSON string.
Example code:
val ROUTE_USER_DETAILS = "user-details?user={user}"
// Pass data (I am using Moshi here)
val user = User(id = 1, name = "John Doe") // User is a data class.
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userJson = jsonAdapter.toJson(user)
navController.navigate(
ROUTE_USER_DETAILS.replace("{user}", userJson)
)
// Receive Data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userJson = backStackEntry.arguments?.getString("user")
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userObject = jsonAdapter.fromJson(userJson)
UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}
// Composable function/view
#Composable
fun UserDetailsView(
user: User
){
// ...
}
I get a weird bug when I implement the top answer to this question. Like I have the following Parcelable to pass between two screens of my Jetpack Compose app:
#Parcelize
data class EdgeParcelable(
val node: NodeParcelable?,
val cursor: String?,
) : Parcelable
And as the accepted Answer says I have implemented a custom NavType:
class EdgeParcelableType : NavType<EdgeParcelable>(isNullableAllowed = false) {
override val name: String
get() = "edge"
override fun get(bundle: Bundle, key: String): EdgeParcelable? {
return bundle.getParcelable(key)
}
override fun parseValue(value: String): EdgeParcelable {
return Gson().fromJson(value, EdgeParcelable::class.java)
}
override fun put(bundle: Bundle, key: String, value: EdgeParcelable) {
bundle.putParcelable(key, value)
}
}
And in my Composable function where I create the NavHost I have:
#Composable
fun MyApp(viewModel: MyViewModel, modifier: Modifier = Modifier) {
val navController = rememberNavController()
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = { MyTopAppBar(
currentScreen=currentScreen,
canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = { navController.navigateUp() }
) }
) { innerPadding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(innerPadding)
) {
composable(route = "home") {
val lazyPagingItems = viewModel.Items()
HomeScreen(
lazyPagingItems = lazyPagingItems,
onTownClicked = { edge: EdgeParcelable ->
val json = Uri.encode(Gson().toJson(edgeParcelable))
navController.navigateSingleTopTo("hotspots/$json")
},
modifier = ...
)
}
composable(
route = "hotspots/{edge}",
arguments = listOf(
navArgument("edge") {
type = EdgeParcelableType()
}
)
) {
val edgeParcelable = it.arguments?.getParcelable<EdgeParcelable>("edge")
HotspotsScreen(edgeParcelable)
}
}
}
}
The code above crashes my Application when I add the lines:
val bsEntry by navController.currentBackStackEntryAsState()
val currentScreen = bsEntry?.destination?.route ?: "Home"
Adding the above lines make the Composable become:
#Composable
fun MyApp(viewModel: MyViewModel, modifier: Modifier = Modifier) {
val navController = rememberNavController()
// Adding these causes a problem...
val bsEntry by navController.currentBackStackEntryAsState()
val currentScreen = bsEntry?.destination?.route ?: "Home"
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = { MyTopAppBar(
currentScreen=currentScreen,
canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = { navController.navigateUp() }
) }
) { innerPadding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(innerPadding)
) {
composable(route = "home") {
val lazyPagingItems = viewModel.Items()
HomeScreen(
lazyPagingItems = lazyPagingItems,
onTownClicked = { edge: EdgeParcelable ->
val json = Uri.encode(Gson().toJson(edgeParcelable))
navController.navigateSingleTopTo("hotspots/$json")
},
modifier = ...
)
}
composable(
route = "hotspots/{edge}",
arguments = listOf(
navArgument("edge") {
type = EdgeParcelableType()
}
)
) {
val edgeParcelable = it.arguments?.getParcelable<EdgeParcelable>("edge")
HotspotsScreen(edgeParcelable)
}
}
}
}
Passing my Custom NavType with the following line of code:
arguments = listOf(navArgument("edge") { type = EdgeParcelableType() } )
now crashes my app, by rendering it unusable. The app seems to choke on itself, almost like, the Navigation API does not really understand the new Custom EdgeParcleableType() or perhaps something is missing that remains to be added to make this EdgeParcelableType work well with the Navigation API.
I was only able to solve the problem by changing the type above to StringType as follows:
arguments = listOf( navArgument("edge") { type = NavType.StringType }
And passing around strings in the rest of the Composable as follows:
#Composable
fun MyApp(viewModel: MyViewModel, modifier: Modifier = Modifier) {
val navController = rememberNavController()
// Using NavType.StringType allows this work...
val bsEntry by navController.currentBackStackEntryAsState()
val currentScreen = bsEntry?.destination?.route ?: "Home"
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = { MyTopAppBar(
currentScreen=currentScreen,
canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = { navController.navigateUp() }
) }
) { innerPadding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(innerPadding)
) {
composable(route = "home") {
val lazyPagingItems = viewModel.Items()
HomeScreen(
lazyPagingItems = lazyPagingItems,
onTownClicked = { edge: EdgeParcelable ->
val json = Uri.encode(Gson().toJson(edgeParcelable))
navController.navigateSingleTopTo("hotspots/$json")
},
modifier = ...
)
}
composable(
route = "hotspots/{edge}",
arguments = listOf( navArgument("edge") {
type = NavType.StringType
}
)
) {
val edgeParcelable = Gson().fromJson(Uri.decode(it.arguments?.getString("edge")), EdgeParcelable::class.java)
HotspotsScreen(edgeParcelable)
}
}
}
}
Then my app worked like a Charm. Sorting this took me like 2 days of trial and error and searching so I hope this can help someone out there if faced with a similar issue...
Following nglauber suggestion, I've created two extensions which are helping me a bit
#Suppress("UNCHECKED_CAST")
fun <T> NavHostController.getArgument(name: String): T {
return previousBackStackEntry?.arguments?.getSerializable(name) as? T
?: throw IllegalArgumentException()
}
fun NavHostController.putArgument(name: String, arg: Serializable?) {
currentBackStackEntry?.arguments?.putSerializable(name, arg)
}
And I use them this way:
Source:
navController.putArgument(NavigationScreens.Pdp.Args.game, game)
navController.navigate(NavigationScreens.Pdp.route)
Destination:
val game = navController.getArgument<Game>(NavigationScreens.Pdp.Args.game)
PdpScreen(game)
A very simple and basic way to do is as below
1.First create the parcelable object that you want to pass e.g
#Parcelize
data class User(
val name: String,
val phoneNumber:String
) : Parcelable
2.Then in the current composable that you are in e.g main screen
val userDetails = UserDetails(
name = "emma",
phoneNumber = "1234"
)
)
navController.currentBackStackEntry?.arguments?.apply {
putParcelable("userDetails",userDetails)
}
navController.navigate(Destination.DetailsScreen.route)
3.Then in the details composable, make sure you pass to it a navcontroller as an parameter e.g.
#Composable
fun Details (navController:NavController){
val data = remember {
mutableStateOf(navController.previousBackStackEntry?.arguments?.getParcelable<UserDetails>("userDetails")!!)
}
}
N.B: If the parcelable is not passed into state, you will receive an error when navigating back
My approach with Moshi:
Routes
sealed class Route(
private val route: String,
val Key: String = "",
) {
object Main : Route(route = "main")
object Profile : Route(route = "profile", Key = "user")
override fun toString(): String {
return when {
Key.isNotEmpty() -> "$route/{$Key}"
else -> route
}
}
}
Extension
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.core.net.toUri
import androidx.navigation.*
import com.squareup.moshi.Moshi
inline fun <reified T> NavController.navigate(
route: String,
data: Pair<String, T>,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
) {
val count = route
.split("{${data.first}}")
.size
.dec()
if (count != 1) {
throw IllegalArgumentException()
}
val out = Moshi.Builder()
.build()
.adapter(T::class.java)
.toJson(data.second)
val newRoute = route.replace(
oldValue = "{${data.first}}",
newValue = Uri.encode(out),
)
navigate(
request = NavDeepLinkRequest.Builder
.fromUri(NavDestination.createRoute(route = newRoute).toUri())
.build(),
navOptions = navOptions,
navigatorExtras = navigatorExtras,
)
}
inline fun <reified T> NavBackStackEntry.getData(key: String): T? {
val data = arguments?.getString(key)
return when {
data != null -> Moshi.Builder()
.build()
.adapter(T::class.java)
.fromJson(data)
else -> null
}
}
#Composable
inline fun <reified T> NavBackStackEntry.rememberGetData(key: String): T? {
return remember { getData<T>(key) }
}
Example usage
data class User(
val id: Int,
val name: String,
)
#Composable
fun RootNavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "${Route.Main}",
) {
composable(
route = "${Route.Main}",
) {
Button(
onClick = {
navController.navigate(
route = "${Route.Profile}",
data = Route.Profile.Key to User(id = 1000, name = "John Doe"),
)
},
content = { Text(text = "Go to Profile") },
}
composable(
route = "${Route.Profile}",
arguments = listOf(
navArgument(name = Route.Profile.Key) { type = NavType.StringType },
),
) { entry ->
val user = entry.rememberGetData<User>(key = Route.Profile.Key)
Text(text = "$user")
}
}
}
you can pass an argument like this
val data = DetailScreenArgument(title = "Sample")
navController.currentBackStackEntry?.savedStateHandle?.apply {
set("detailArgument", data)
}
navController.navigate(Screen.DetailScreen.route)
and get the argument in the destination like this
val detailArgument = navController.previousBackStackEntry?.savedStateHandle?.get<DetailScreenArgument>(
"detailArgument"
)
You can use my own solution: https://github.com/usmonie/compose_navigation_with_parcelable_arguments
With my solution, you will be able to get parcelable values directly into the composable function
I had a similar issue where I had to pass a string that contains slashes, and since they are used as separators for deep link arguments I could not do that. Escaping them didn't seem "clean" to me.
I came up with the following workaround, which can be easily tweaked for your case. I rewrote NavHost, NavController.createGraph and NavGraphBuilder.composable from androidx.navigation.compose as follows:
#Composable
fun NavHost(
navController: NavHostController,
startDestination: Screen,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) {
NavHost(navController, remember(route, startDestination, builder) {
navController.createGraph(startDestination, route, builder)
})
}
fun NavController.createGraph(
startDestination: Screen,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) = navigatorProvider.navigation(route?.hashCode() ?: 0, startDestination.hashCode(), builder)
fun NavGraphBuilder.composable(
screen: Screen,
content: #Composable (NavBackStackEntry) -> Unit
) {
addDestination(ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
id = screen.hashCode()
})
}
Where Screen is my destination enum
sealed class Screen {
object Index : Screen()
object Example : Screen()
}
Please note that I removed deep links and arguments since I am not using them. That will still allow me to pass and retrieve arguments manually, and that functionality can be re-added, I simply didn't need it for my case.
Say I want Example to take a string argument path
const val ARG_PATH = "path"
I then initialise the NavHost like so
NavHost(navController, startDestination = Screen.Index) {
composable(Screen.Index) { IndexScreen(::navToExample) }
composable(Screen.Example) { navBackStackEntry ->
navBackStackEntry.arguments?.getString(ARG_PATH)?.let { path ->
ExampleScreen(path, ::navToIndex)
}
}
}
And this is how I navigate to Example passing path
fun navToExample(path: String) {
navController.navigate(Screen.Example.hashCode(), Bundle().apply {
putString(ARG_PATH, path)
})
}
I am sure that this can be improved, but these were my initial thoughts.
To enable deep links, you will need to revert back to using
// composable() and other places
val internalRoute = "android-app://androidx.navigation.compose/$route"
id = internalRoute.hashCode()
Since the nglauber's answer work when going forward and does not when navigating backward and you get a null. I thought maybe at least for the time being we can save the passed argument using remember in our composable and be hopeful that they add the Parcelable argument type to the navigating with the route.
the destination composable target:
composable("yourRout") { backStackEntry ->
backStackEntry.arguments?.let {
val rememberedProject = remember { mutableStateOf<Project?>(null) }
val project =
navController.previousBackStackEntry?.arguments?.getParcelable(
PROJECT_ARGUMENT_KEY
) ?: rememberedProject.value
rememberedProject.value = project
TargetScreen(
project = project ?: throw IllegalArgumentException("parcelable was null"),
)
}
And here's the the source code: to trigger the navigation:
navController.currentBackStackEntry?.arguments =
Bundle().apply {
putParcelable(PROJECT_ARGUMENT_KEY, project)
}
navController.navigate("yourRout")
1 - Add the name of the data sent in the route
const val NEWS_DETAILS = "NEWS_DETAILS/{article}/{name}"
2 - Create dataClassParcelable
#Parcelize
data class ArticlesModel(
#SerializedName("author")
val author: String?,
#SerializedName("content")
val content: String?,
#SerializedName("description")
val description: String?,
#SerializedName("publishedAt")
val publishedAt: String?,
#SerializedName("source")
val source: Strins?,
#SerializedName("title")
val title: String?,
#SerializedName("url")
val url: String?,
#SerializedName("urlToImage")
val urlToImage: String?
):Parcelable
3 - Submit the required data
fun NavController.openNewsDetails(article: ArticlesModel,name:String) {
currentBackStackEntry?.savedStateHandle?.apply{
set(
"article",
article
)
set(
"name",
name
)
}
navigate(ScreenConst.NEWS_DETAILS)
}
4 - Reading information and sending them to a new screen
composable(
route = ScreenConst.NEWS_DETAILS
) { navBackStackEntry ->
val article =
navController.previousBackStackEntry?.savedStateHandle?.get<ArticlesModel("article")
val name=
navController.previousBackStackEntry?.savedStateHandle?.get<String>("name")
if (article==null && name.isNullOrEmpty()) {
return#composable
}
DetailsScreen(
article = article,
name = name
)
}

Categories

Resources