24

So far I'm successfully able to navigate to dialogs and back using only navigation component. The problem is that, I have to do some stuff in dialog and return result to the fragment where dialog was called from.

One way is to use shared viewmodel. But for that I have to use .of(activity) which leaves my app with a singleton taking up memory, even when I no longer need it.

Another way is to override show(fragmentManager, id) method, get access to fragment manager and from it, access to previous fragment, which could then be set as targetfragment. I've used targetFragment approach before where I would implement a callback interface, so my dialog could notify targetFragment about result. But in navigation component approach it feels hacky and might stop working at one point or another.

Any other ways to do what I want? Maybe there's a way to fix issue on first approach?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
SMGhost
  • 3,867
  • 6
  • 38
  • 68
  • See the [existing feature request](https://issuetracker.google.com/issues/79672220) for navigating for a result. – ianhanniballake Jun 17 '19 at 05:12
  • 1
    @SMGhost what I did was to create a custom DialogFragment with LiveData + data binding. So if I have for example a String that the calling fragment needs, the calling fragment can just observe that LiveData, like ```baseDialog.someText.observe(getViewLifecycleOwner(), string -> { // do your stuff});``` – Alvin Dizon Jul 04 '19 at 01:03
  • @SMGhost do you solve that problem? – Vahan Sep 12 '19 at 09:04
  • 3
    Hi, @Vahan, I ended up using viewmodel for this. One caveat is that before I go to dialog I need to make sure I call some reset method on my view model, so I wouldn't end up using data from previous times dialog was opened. – SMGhost Sep 12 '19 at 09:14
  • @SMGhost Hi) I also stand on using shared viewmodel, you don't need to reset view model value, if you are using LiveData when you postvalue to it, it automatically removes previous value – Vahan Sep 12 '19 at 13:17
  • @Vahan, not necessarily. Sometimes you need to have default values when you open your fragment/dialog, or you just can't use the previous ones. So you can't really open dialog with old data and show it while waiting for new data to arrive. – SMGhost Sep 12 '19 at 13:45
  • https://developer.android.com/guide/navigation/navigation-pass-data – eli Nov 05 '19 at 14:01
  • I mean, granted. This isn't OP's fault but the sheer presence of this question and answers to it just means how ridiculous is getting Android development. Why we should do all this for just a simple task is beyond me. – YaMiN Feb 23 '22 at 20:27

3 Answers3

25

Thanks to @NataTse and also the official docs, i came up with the extensions so that hopefully less boilerplate code to write:

fun <T>Fragment.setNavigationResult(key: String, value: T) {
    findNavController().previousBackStackEntry?.savedStateHandle?.set(
        key,
        value
    )
}

fun <T>Fragment.getNavigationResult(@IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
    val navBackStackEntry = findNavController().getBackStackEntry(id)

    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_RESUME
            && navBackStackEntry.savedStateHandle.contains(key)
        ) {
            val result = navBackStackEntry.savedStateHandle.get<T>(key)
            result?.let(onResult)
            navBackStackEntry.savedStateHandle.remove<T>(key)
        }
    }
    navBackStackEntry.lifecycle.addObserver(observer)

    viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) {
            navBackStackEntry.lifecycle.removeObserver(observer)
        }
    })
}
dumbfingers
  • 7,001
  • 5
  • 54
  • 80
  • 3
    This is THE BEST extension function(s) I've found for dealing with this. Specifically including `navBackStackEntry.savedStateHandle.remove(key)`. Lifesaver! – Nelson.b.austin Jan 21 '21 at 04:34
  • @billygates for dialogfragment maybe using a shared ViewModel with the host fragment is a better solution for passing the results. – dumbfingers Apr 01 '21 at 09:57
  • @billygates for dialogFragments, I've found defining an interface with a callback to be the most useful. – gOnZo May 13 '21 at 11:58
  • 1
    Thumbs up for the remove key!!!. Also another improvement could be using "findNavController().currentDestination.id" surrounded by a let if you don't want to explicitly give the destination id asa a param – RFM Aug 06 '21 at 08:06
21

In Navigation 2.3.0-alpha02 and higher, NavBackStackEntry gives access to a SavedStateHandle. A SavedStateHandle is a key-value map that can be used to store and retrieve data. These values persist through process death, including configuration changes, and remain available through the same object. By using the given SavedStateHandle, you can access and pass data between destinations. This is especially useful as a mechanism to get data back from a destination after it is popped off the stack.

To pass data back to Destination A from Destination B, first set up Destination A to listen for a result on its SavedStateHandle. To do so, retrieve the NavBackStackEntry by using the getCurrentBackStackEntry() API and then observe the LiveData provided by SavedStateHandle.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController();
// We use a String here, but any type that can be put in a Bundle is supported
navController.currentBackStackEntry?.savedStateHandle?.getLiveData("key")?.observe(
    viewLifecycleOwner) { result ->
    // Do something with the result.
}

}

In Destination B, you must set the result on the SavedStateHandle of Destination A by using the getPreviousBackStackEntry() API.

navController.previousBackStackEntry?.savedStateHandle?.set("key", result)
Hamed safari
  • 295
  • 4
  • 6
8

When you use Navigation Component with dialogs, this part of code looks not so good (for me it returned nothing)

navController.currentBackStackEntry?.savedStateHandle?.getLiveData("key")?.observe(
viewLifecycleOwner) { result ->
// Do something with the result.}

You need to try way from official docs and it help me a lot

This part is working for me:

 val navBackStackEntry = navController.getBackStackEntry(R.id.target_fragment_id)

    // Create observer and add it to the NavBackStackEntry's lifecycle
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_RESUME
            && navBackStackEntry.savedStateHandle.contains("key")
        ) {
            val result =
                navBackStackEntry.savedStateHandle.get<Boolean>("key")
            // Do something with the result

        }
    }
    navBackStackEntry.lifecycle.addObserver(observer)

    // As addObserver() does not automatically remove the observer, we
    // call removeObserver() manually when the view lifecycle is destroyed
    viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) {
            navBackStackEntry.lifecycle.removeObserver(observer)
        }
    })

And in your dialog:

navController.previousBackStackEntry?.savedStateHandle?.set(
            "key",
            true
        )
NataTse
  • 381
  • 2
  • 10
  • Thanks for posting. It shed some light on me while I was trying to solve the same problem. However there's a slight problem: the `R.id.target_fragment_id` is the dialog's, so the lifecycle event is actually the dialog's life cycle event. When you click a button in a dialog to pass the data and then dismiss the dialog, this method above might not work properly, as the event at this point (click save data & dismiss) will be quickly OnPause, then onDestroy. I think using a shared view model might be a better solution in my case? – dumbfingers Jul 28 '20 at 10:05
  • 1
    @dumbfingers yeap, i agree, shared view model is good idea, but i had case when i dont use architecture with viewmodel, and i am not sure is it correct or trick (or my mistake:/), but i use `R.id.target_fragment_id` as fragment that need to handle event from dialog, let say R.id.nice_detail_fragment, not a R.id.some_dialog_fragment, if you mean it, please correct me:) – NataTse Jul 28 '20 at 13:00
  • so the `target_fragment_id` should be the one who launches the dialog, right? I believe I misunderstood the documentation as it says it needs the destination's id? lol Next time I'll try this method, but I've already switched to use the SharedViewModel (it's also recommended by Google, but I also felt it's not 100% correct way to do it) – dumbfingers Jul 28 '20 at 13:06
  • 5
    An update: the ShareViewModel doesn't work out nice. So I tried your method again and make sure the `target_fragment_id` is the source fragment not the destination fragment's id, and I can confirm, it works. Thanks and that really helped. – dumbfingers Jul 28 '20 at 14:05
  • Your comments saved me @dumbfingers – Ayia Feb 18 '22 at 16:10
  • @Ayia glad it helped :) – dumbfingers Feb 18 '22 at 16:14