Jetpack Compose Sending Result Back with SavedStateHandle does not work with SavedStateHandle injected in ViewModel
N

1

9

Sending Result Back with SavedStateHandle does not work with SavedStateHandle injected in ViewModel.

Getting result using navController.currentBackStackEntry?.savedStateHandle? it works!

fun CreatePostScreen(
    navController: NavController,
    coroutineScope: CoroutineScope,
    snackbarState: SnackbarHostState,
    viewModel: CreatePostViewModel = hiltViewModel(),
) {

    LaunchedEffect(key1 = Unit) {

        navController.currentBackStackEntry?.savedStateHandle?.getStateFlow(
            "result", ""
        )?.collect { result ->
            Timber.d("Result -> $result")
        }
    }
}

Using saveStateHandle injected using Hilt in ViewModel doesn't get the result!

@HiltViewModel
class CreatePostViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
    
    init {

        viewModelScope.launch {
            savedStateHandle.getStateFlow("result", "").collect {
                Timber.d("Result -> $it")
            }
        }
    }
}

That's how I'm sending the result back to the previous screen!

navController.previousBackStackEntry?.savedStateHandle?.set("result", "this is result")
Naquin answered 13/8, 2023 at 6:57 Comment(0)
U
16

The important thing to realize is that every ViewModel instance gets its own SavedStateHandle - if you accessed two separate ViewModel classes on the same screen, they would each have their own SavedStateHandle.

So when you call navController.currentBackStackEntry?.savedStateHandle, you aren't actually getting the SavedStateHandle associated with your CreatePostViewModel - if you look at the NavBackStackEntry source code, you'll note that the SavedStateHandle it is returning is for a private ViewModel subclass that is completely independent of any other ViewModels you create.

Therefore if you want to send a result back specifically to your own custom ViewModel (like your CreatePostViewModel), you need to specifically ask for exactly that ViewModel in your other screen:

// Assumes `it` is the current NavBackStackEntry that was passed to you
// from the composable() lambda
val previousBackStackEntry = remember(it) {
  navController.previousBackStackEntry!!
}
val previousViewModel = hiltViewModel<CreatePostViewModel>(previouslyBackStackEntry)
previousViewModel.savedStateHandle?.set("result", "this is result")

Note that with this approach, you need to specifically ask for the ViewModel by its exact class name - that's because the class name is the default key that is passed to the viewModel() method and similarly for hiltViewModel().

Unshod answered 14/8, 2023 at 20:32 Comment(5)
But does your explanation of the problem imply that we cannot generalize popWithResult behavior and are doomed to implement ViewModel-specific logic provided we do not want to create a kind of BaseViewModel with public SavedStateHandle?Hoon
And furthermore it necessarily means that we must expose ViewModel's state mutation APIs (savedStateHandle.set(...)), which is considered bad practice, as UI can theoretically do whatever it wants with it. What do you suppose can one do to combat this?Hoon
@Hoon - that is indeed why you don't see any documentation or official guides that recommend sending results to a specific instance of a ViewModel that allows external classes to mutate it and precisely why the NavBackStackEntry.savedStateHandle API exists - to offer that generalized approach that doesn't tightly couple screen together.Unshod
Is there a complete working example not using hilt? I tried using viewModel(key=previousBackStackEntry.toString()) but it was creating a new ViewModel instance.Aerostatics
@TomBerghuis - hiltViewModel and viewModel work exactly the same, so you'd use viewModel<CreatePostViewModel>(previousBackStackEntry).Unshod

© 2022 - 2024 — McMap. All rights reserved.