Hey guys, i tried to set a result with `navigatio...
# compose
a
Hey guys, i tried to set a result with
navigation-compose
and
hiltViewModel
. Any ideas, why my example never gets a result? The
savedStateHandle
within my
viewModel
never gets the result value. I only get the result directly from
navBackStackEntry.savedStateHandle
. Example in second post
Copy code
@Composable
fun DemonstartionApp() {
    val navHostController = rememberNavController()
    Surface(modifier = Modifier
        .fillMaxSize()
        .statusBarsPadding()
    ) {
        NavHost(navController = navHostController, startDestination = "screen1") {
            composable(
                route = "screen1"
            ) { navBackStackEntry ->
                val viewModel = viewModel<Screen1ViewModel>(factory = HiltViewModelFactory(LocalContext.current, navBackStackEntry))
                val id by viewModel.selectedId.observeAsState()

                val idFromNavBackStackEntry: String? = remember(navBackStackEntry.savedStateHandle) {
                    navBackStackEntry.savedStateHandle.get<String>("selectedId")
                }

                Surface {
                    Column {
                        Text(text = id ?: "No id from hiltViewModel savedStateHandle")
                        Text(text = idFromNavBackStackEntry ?: "No id from navBackStackEntry savedStateHandle")

                        Button(onClick = {
                            navHostController.navigate("screen2")
                        }) {
                            Text(text = "Select ID")
                        }
                    }
                }
            }
            composable(
                route = "screen2"
            ) {
                Surface{
                    Column {
                        Button(onClick = {
                            navHostController.previousBackStackEntry?.savedStateHandle?.set("selectedId", "ID_1")
                            navHostController.popBackStack()
                        }) {
                            Text(text = "ID 1")
                        }
                        Button(onClick = {
                            navHostController.previousBackStackEntry?.savedStateHandle?.set("selectedId", "ID_2")
                            navHostController.popBackStack()
                        }) {
                            Text(text = "ID 2")
                        }
                    }
                }
            }
        }
    }
}

@HiltViewModel
class Screen1ViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
): ViewModel() {
    val selectedId: LiveData<String> = savedStateHandle.getLiveData("selectedId")
}
The
savedStateHandle
within the
viewModel
and
navBackStackEntry.savedStateHandle
in screen1 are not the same objects. Thats the reason, why the
LiveData<String>
in the
viewModel
never triggerts. @Ian Lake Is that the way it's supposed to be?
i
The
navBackStackEntry.savedStateHandle
is not used in your own custom ViewModel (every ViewModel gets its own
SavedStateHandle)
, so it is expected that those are completely different objects
Note that you can and should be using the
hiltNavGraphViewModel()
method for retrieving Hilt provided ViewModels. Those APIs would allow you to pass
navHostController.previousBackStackEntry
in, thus giving your other destination direct access to your same
Screen1ViewModel
instance
a
Ah ok, never thought about that way. You mean, get Screen1ViewModel in Screen2 with
hiltNavGraphViewModel(
navHostController.previousBackStackEntry)
and set the value directly to it?
i
That would be how you'd access that exact ViewModel and that exact
SavedStateHandle,
which seems like what you are trying to do?
a
I tried to implement a feature, where my gallery screen is used to select a picture. The pictureId should be passed as a result to the previous destination. In my current implementation, i set the selected pictureId with
navHostController.previousBackStackEntry?.savedStateHandle?.set("selectedPictureId", pictureId)
and the previous destionation get it from
navBackStackEntry
and set it to its viewModel.
Copy code
val selectedPictureId: String? = remember(navBackStackEntry.savedStateHandle) {
    navBackStackEntry.savedStateHandle.get<String>("selectedPictureId")
}

selectedPictureId?.let {
    viewModel.setSelectedPictureId(it)
}
And than i saw, that the savedStateHandle provides LiveData functions and i thought, that should work to observe on the savedStateHandle within the viewModel of the previous destination... Seems not to be.
But when i get the previous viewModel within my gallery, i add a dependency to it, that should not exists. Maybe my current implementation but with LaunchedEffect would be the best solution?
Copy code
LaunchedEffect(navBackStackEntry.savedStateHandle) {
    navBackStackEntry.savedStateHandle.get<String>("selectedPictureId")?.let {
        viewModel.setSelectedPictureId(it)
    }
}
i
I think you should try a more reactive approach:
Copy code
val selectedPictureId = navBackStackEntry.savedStateHandle.getLiveData<String>("selectedPictureId").observeAsState()
if (selectedPictureId == null) {
  // Show "select a picture UI
} else {
  val dataFromViewModel = viewModel.getPicture(selectedPictureId).collectAsState()
  // Now use the data to fill in your UI
}
a
In my case, i want to do more than show the selected picture in the previous destination. I also want to set the new pictureId in my aquarium object and update the aquarium cloud object also. And of course show the new selected picture in the UI.
i
Then yeah, the
LaunchedEffect
route might be better for processing the selected item. I'd make sure you call
savedStateHandle.remove("selectedPictureId")
after processing it if you are treating it like an event you need to process only once per selection and not like state
👍 1