118

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?

7

9 Answers 9

117

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)
}
Sign up to request clarification or add additional context in comments.

14 Comments

This should be accepted as the correct answer.so simple and do the work perfectly
and maybe it will be useful for someone with Navigation Component and dialogs stackoverflow.com/a/62054347/7968334
java.lang.IllegalArgumentException: Can't put value with type class com.experienceapi.auth.models.LogonModel into saved state . Any idea?
Beware currentBackStackEntry will not work correctly with <dialog> destinations (see docs), you need to use getBackStackEntry() with the ID of your destination then
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.
|
109

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

5 Comments

This problem exists with your solution: stackoverflow.com/q/63669116/3248593
Very interesting and simple decision, it is was I need to return data from child fragment
@MortezaRastgoo I think that problem can be solved with viewmodel or any persisting data strategy
not working for me, Fragment A result listener never called and don't know why
not working here either.
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.

6 Comments

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 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: getPreviousBackStackEntry - return the previous visible entry on the back stack or null if the back stack has less than two visible entries.
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
If its not working for you . Try replacing scope from viewLifecycleOwner with findNavController().currentBackStackEntry
this way has an issue if the user come back to first Fragmment again the live data called twice or more, use this post stackoverflow.com/a/69005732/4797289
|
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.
        });
    }
}

9 Comments

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.
what if we don't have view model ?
@CarsonHolzheimer what is parent here?
@CarsonHolzheimer the problem you saying solved here stackoverflow.com/questions/55137338/…
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.
|
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)
}

Comments

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.

Comments

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)
}

1 Comment

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]stackoverflow.com/questions/how-to-answer)
-3

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.

1 Comment

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: medium.com/@rufuszh90/…
-3

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.