115

Let's say that we have two fragments: MainFragment and SelectionFragment. The second one is build for selecting some object, e.g. an integer. There are different approaches in receiving result from this second fragment like callbacks, buses etc.

Now, if we decide to use Navigation Architecture Component in order to navigate to second fragment we can use this code:

NavHostFragment.findNavController(this).navigate(R.id.action_selection, bundle)

where bundle is an instance of Bundle (of course). As you can see there is no access to SelectionFragment where we could put a callback. The question is, how to receive a result with Navigation Architecture Component?

Nominalista
  • 4,632
  • 11
  • 43
  • 102
  • 1
    Have `SelectionFragment` update a shared `ViewModel` (directly or indirectly), where `MainFragment` is subscribed to find out about changes in that `ViewModel`. – CommonsWare Jun 08 '18 at 10:56
  • 1
    Provided that you're using `ViewModel`, which is not related to Navigation Component. – Nominalista Jun 08 '18 at 11:00
  • 4
    Correct. They are designed to work together, and [Google is indicating that a shared `ViewModel` is the recommended way to communicate between fragments when using the Navigation library](https://stackoverflow.com/a/50752558/115145). – CommonsWare Jun 08 '18 at 11:02
  • 2
    I think you should post it as answer. – Nominalista Jun 08 '18 at 11:03
  • 1
    Use graph scoped shared view models https://stackoverflow.com/questions/55137338/android-navigation-component-with-shared-view-models – Mukhtar Bimurat Nov 04 '19 at 12:34
  • use `fragmentManager.putFragment(bundle, TargetFragment.EXTRA_CALLER, this)` before navigating, then in the target fragment find the called fragment `fragmentManager?.getFragment(args, EXTRA_CALLER)` – Pnemonic Nov 24 '19 at 14:59
  • use this way https://stackoverflow.com/a/69005732/4797289 – Rasoul Miri Aug 31 '21 at 21:59

9 Answers9

112

They have added a fix for this in the 2.3.0-alpha02 release.

If navigating from Fragment A to Fragment B and A needs a result from B:

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

If on Fragment B and need to set the result:

findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result)

I ended up creating two extensions for this:

fun Fragment.getNavigationResult(key: String = "result") =
    findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(key)

fun Fragment.setNavigationResult(result: String, key: String = "result") {
    findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
LeHaine
  • 1,348
  • 1
  • 12
  • 16
  • 6
    This should be accepted as the correct answer.so simple and do the work perfectly – joghm Apr 16 '20 at 11:23
  • and maybe it will be useful for someone with Navigation Component and dialogs https://stackoverflow.com/a/62054347/7968334 – NataTse May 27 '20 at 23:20
  • 1
    java.lang.IllegalArgumentException: Can't put value with type class com.experienceapi.auth.models.LogonModel into saved state . Any idea? – kgandroid Jun 13 '20 at 21:35
  • Make sure to implement the Parcelable interface in order to save custom objects. – LeHaine Jun 14 '20 at 01:08
  • Thanks for your reply.Implemented it but still the same result.Should I write findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result) in the writeParcel() method? Please reply. – kgandroid Jun 14 '20 at 15:21
  • https://stackoverflow.com/questions/62343552/livedata-not-able-to-observe-the-changes – kgandroid Jun 14 '20 at 15:27
  • It is very simple and useful. – roghayeh hosseini Nov 01 '20 at 12:41
  • it's 2.3.0-alpha02 version? or 1.3.0-alpha02 – LunaVulpo Nov 30 '20 at 08:32
  • From documentation: The value must have a type that could be stored in android.os.Bundle – Valentin Yuryev Dec 08 '20 at 15:12
  • 4
    Beware `currentBackStackEntry` will not work correctly with `` destinations (see [docs](https://developer.android.com/guide/navigation/navigation-programmatic#additional_considerations)), you need to use `getBackStackEntry()` with the ID of your destination then – arekolek Mar 04 '21 at 20:56
  • 1
    It has a problem. If you navigate from Fragment B to Fragment A. Fragment A obtains the data from live data, which is fine. But if you restart the fragment (configuration change), then observer works again, because live data has data cached. – Andrew Apr 30 '21 at 11:39
  • @Andrew just remove the observer after data was consumed. – vitaliy.gerasymchuk May 28 '21 at 11:30
  • any solution for live data caching the value, the issue is After moving from fragment B to A with a value, I am moving to Fragment C from A and when I press back button fragment A has the same value in which I get from B – ranjith Sep 21 '22 at 11:03
  • Using this logic, I am able navigate back to fragment A with the arguments, but after that it won't let me go back to fragment B. It says the navController.currentDestination = null. – Mayur More Jul 21 '23 at 05:16
101

Since Fragment KTX 1.3.6 Android supports passing data between fragments or between fragments and activities. It's similar to startActivityForResult logic.

Here is an example with Navigation Component. You can read more about it here

build.gradle

implementation "androidx.fragment:fragment-ktx:1.3.6"

FragmentA.kt

class FragmentA : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        // Step 1. Listen for fragment results
        setFragmentResultListener(FragmentB.REQUEST_KEY) { key, bundle -> 
            // read from the bundle
        }

        // Step 2. Navigate to Fragment B
        findNavController().navigate(R.id.fragmentB)
    }
}

FragmentB.kt

class FragmentB : Fragment() {

    companion object {
        val REQUEST_KEY = "FragmentB_REQUEST_KEY"
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        buttonA.setOnClickListener { view -> 
            // Step 3. Set a result
            setFragmentResult(REQUEST_KEY, bundleOf("data" to "button clicked"))
            
            // Step 4. Go back to Fragment A
            findNavController().navigateUp()
        }
    }
}    
arsent
  • 6,975
  • 3
  • 32
  • 31
43

Use these extension functions

fun <T> Fragment.getNavigationResult(key: String = "result") =
    findNavController().currentBackStackEntry?.savedStateHandle?.get<T>(key)

fun <T> Fragment.getNavigationResultLiveData(key: String = "result") =
        findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)

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

So if you want to send result from Fragment B to fragment A

Inside Fragment B

setNavigationResult(false, "someKey")

Inside Fragment A

val result = fragment.getNavigationResultLiveData<Boolean>("someKey")
result.observe(viewLifecycleOwner){ booleanValue-> doSomething(booleanValue)

Important note

In the Fragment B you need to set the result (setNavigationResult()) in the started or resumed state (before onStop() or onDestroy()), otherwise previousBackStackEntry will be already null.

Important note #2

If you’d only like to handle a result only once, you must call remove() on the SavedStateHandle to clear the result. If you do not remove the result, the LiveData will continue to return the last result to any new Observer instances.

More information in the official guide.

Tomas
  • 4,652
  • 6
  • 31
  • 37
Crazy
  • 576
  • 6
  • 5
  • Easy, simple and fast! Thanks a lot.. amazing! – Pierry Oct 21 '20 at 14:59
  • this is great answer – reza_khalafi Mar 07 '21 at 08:53
  • 2
    I tried using savedStateHandle and was able to set the key with result but I never received the update in my observer.. I am using version 2.3.4... – shadygoneinsane Apr 04 '21 at 12:25
  • @shadygoneinsane I experienced the same issue. When I tried to debug it, I found that is very important in what lifecycle is `navController.previousBackStackEntry` called. For example in `onStop()` or `onDestroy()` is the `previousBackStackEntry` already `null`. So you need to set the result before. According to [this documentation](https://developer.android.com/reference/androidx/navigation/NavController#getPreviousBackStackEntry()): `getPreviousBackStackEntry` - return the previous visible entry on the back stack or null if the back stack has less than two visible entries. – Tomas Apr 19 '21 at 09:12
  • Notice that if. "result.observe(viewLifecycleOwner){ booleanValue-> doSomething(booleanValue)}" does not resolve to boolean, place the function inside parenthesis like this: "observe(viewLifecycleOwner, Observer { booleanValue -> })". Happy codingg – Hanako Jun 16 '21 at 01:36
  • If its not working for you . Try replacing scope from viewLifecycleOwner with findNavController().currentBackStackEntry – Jan Aug 05 '21 at 08:23
  • this way has an issue if the user come back to first Fragmment again the live data called twice or more, use this post https://stackoverflow.com/a/69005732/4797289 – Rasoul Miri Aug 31 '21 at 22:01
  • @Tomas thanks. I faced lifecycle issue too and by changing the owner of observer result start showing just fine. – Master mj Apr 23 '23 at 14:12
30

According to Google: you should try to use shared ViewModel. Check below example from Google:

Shared ViewModel that will contain shared data and can be accessed from different fragments.

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

MasterFragment that updates ViewModel:

public class MasterFragment extends Fragment {

    private SharedViewModel model;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

DetailsFragment that uses shared ViewModel:

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, item -> {
           // Update the UI.
        });
    }
}
Amir Latifi
  • 798
  • 8
  • 15
  • 25
    The problem with this is you are scoping your viewmodel to the activity with `getActivity()`. This means it will never be cleared (uses extra memory and could cause unexpected results when you navigate back to these fragments later, and stale data is shown). You should use `ViewModelProviders.of(parent)...` instead. – Carson Holzheimer Feb 20 '19 at 00:11
  • 3
    what if we don't have view model ? – Cyph3rCod3r Apr 29 '19 at 12:14
  • 2
    @CarsonHolzheimer what is parent here? – Anup Ammanavar May 24 '19 at 14:31
  • 2
    @CarsonHolzheimer the problem you saying solved here https://stackoverflow.com/questions/55137338/android-navigation-component-with-shared-view-models – Mukhtar Bimurat Nov 04 '19 at 12:32
  • 1
    I wouldn't say solved. They are some less than ideal workarounds. Shared viewmodels in general aren't always a good idea. For simply returning a result to a previous activity they hard to read and more difficult to build on than the upcoming API for returning with a result. – Carson Holzheimer Nov 04 '19 at 12:46
  • "the upcoming API for returning with a result." Does this API now exist? If so, got a link? @CarsonHolzheimer – Graham Perks Dec 05 '19 at 18:18
  • Sorry it still doesn't exist. It's been a long time coming. Search for startactivityforresult analog on Google issue tracker – Carson Holzheimer Dec 05 '19 at 22:59
  • What if I don't want to consider using this approach? – Cyph3rCod3r Mar 14 '20 at 10:50
  • you should use private val model: SharedViewModel by activityViewModels() – Muhammed shamshad p Sep 26 '21 at 11:57
6

with a little improvement of @LeHaine 's answer, you can use these methods for navigation 2.3.0-alpha02 and above

fun <T> Fragment.getNavigationResult(key: String) =
    findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)

fun <T> Fragment.setNavigationResult(result: T, key: String) {
    findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
Mustafa Ozhan
  • 321
  • 6
  • 9
1

I created a wrapper function that's similar to LeHaine's answer, but it handles more cases.

To pass a result to a parent from a child:

findNavController().finishWithResult(PickIntervalResult.WEEKLY)

To get result from a child in a parent:

findNavController().handleResult<PickIntervalResult>(
    viewLifecycleOwner,
    R.id.navigation_notifications, // current destination
    R.id.pickNotificationIntervalFragment // child destination
    ) { result ->
        binding.textNotifications.text = result.toString()
}

My wrapper isn't so simple as LeHaine's one, but it is generic and handles cases as:

  1. A few children for one parent
  2. Result is any class that implements Parcelable
  3. Dialog destination

See the implementation on the github or check out an article that explains how it works.

VadzimV
  • 1,111
  • 12
  • 13
0
fun <Type> BaseNavigationActivity<*,*,*>.getNavigationResult(key : String, @IdRes viewId: Int)  =
    this.findNavController(viewId).currentBackStackEntry?.savedStateHandle?.getLiveData<Type>(key)


fun <Type> BaseNavigationActivity<*,*,*>.setNavigationResult(result: Type, key: String, @IdRes viewId: Int){
   this.findNavController(viewId).previousBackStackEntry?.savedStateHandle?.set<Type>(key, result)
}
  • 5
    Welcome to Stack Overflow. Code is a lot more helpful when it is accompanied by an explanation. Stack Overflow is about learning, not providing snippets to blindly copy and paste. Please edit your answer and explain how it answers the specific question being asked. See [How to Answer]https://stackoverflow.com/questions/how-to-answer) – Vimal Patel Dec 16 '21 at 16:35
-2

I would suggest you to use NavigationResult library which is an add-on for JetPack's Navigation Component and lets you to navigateUp with Bundle. I've also wrote a blog post about this on Medium.

Mahdi Nouri
  • 1,391
  • 14
  • 29
  • 3
    I wouldn't recommend this library as it highly relies on inheritance. Sooner or later this will limit you in your endeavour. TL;DR Favor composition of inheritance: https://medium.com/@rufuszh90/effective-java-item-16-favour-composition-over-inheritance-ed82e482fd1a – Mehlyfication Dec 30 '19 at 12:17
-2

Just an alternative to the other answers...

EventBus with MutableShareFlow as it core in a shared object (ex: repo) and an observer describe in here

Looks like things are moving away from LiveData and going in Flow direction.

Worth to have a look.

HDN
  • 93
  • 2
  • 9