1

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")
Malik Bilal
  • 869
  • 8
  • 25

1 Answers1

8

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().

ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • 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`? – Calamity Aug 30 '23 at 07:54
  • 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? – Calamity Aug 30 '23 at 08:01
  • @Calamity - 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. – ianhanniballake Aug 30 '23 at 19:16