59

I have a workflow with 3 screens. From "screen 1" to access to "screen 2", the user must accept some kind of terms and conditions that I call in my picture "modal". But he only has to accept those conditions once. The next time he is on the first screen, he can go directly to screen 2. The user can chose to NOT accept the terms, and therefore we go back to "screen 1" and do not try to go to "screen 2".

App workflow

I am wondering how to do it with the new navigation component.

Previously, what I would do it:

  • On screen 1, check if the user must accept the conditions
  • If no, start "screen 2" activity
  • If yes, use startActivityForResult() and wait result from the modal. Mark the terms as accepted. Start "screen 2"

But with the navigation graph, there is no way to start a Fragment to obtain a result.

I could mark the terms as accepted in the "modal" screen and start the "screen 2" from there. The thing is that to access to the screen 2, I need to do a network request. I do not want to duplicate the call to the API and processing its outcome in both "screen 1" and "modal".

Is there a way to go back from "modal" to "screen 1" with some information (user accepted the terms), using Jetpack navigation?

Edit: I currently get around it by using the same flow that Yahya suggests below: using an Activity just for the modal and using startActivityForResult from the "screen 1". I am just wondering if I could continue to use the navigation graph for the whole flow.

Jonas Schmid
  • 5,360
  • 6
  • 38
  • 60
  • 1
    One year later, there is correct way to implement it? I tried accepted answer but there is no such methods, the api changed? serialized callback is very bad solution – Pavel Poley Apr 21 '20 at 10:07

6 Answers6

23

In the 1.3.0-alpha04 version of AndroidX Fragment library they introduced new APIs that allow passing data between Fragments.

Added support for passing results between two Fragments via new APIs on FragmentManager. This works for hierarchy fragments (parent/child), DialogFragments, and fragments in Navigation and ensures that results are only sent to your Fragment while it is at least STARTED. (b/149787344)

FragmentManager gained two new methods:

How to use it?

In FragmentA add FragmentResultListener to the FragmentManager in the onCreate method:

setFragmentResultListener("request_key") { requestKey: String, bundle: Bundle ->
    val result = bundle.getString("your_data_key")
    // do something with the result
}

In FragmentB add this code to return the result:

val result = Bundle().apply {
    putString("your_data_key", "Hello!")
}
setFragmentResult("request_key", result)

Start FragmentB e.g.: by using:

findNavController().navigate(NavGraphDirections.yourActionToFragmentB())

To close/finish FragmentB call:

findNavController().navigateUp()

Now, your FragmentResultListener should be notified and you should receive your result.

(I'm using fragment-ktx to simplify the code above)

Ziem
  • 6,579
  • 8
  • 53
  • 86
  • How come I don't see `setFragmentResultListener` in FragmentManager anymore, yet it exists on the docs: https://developer.android.com/reference/kotlin/androidx/fragment/app/FragmentManager#setfragmentresultlistener ? I've tried both v2.2.2 and v2.3.0-rc01 . – android developer Jun 23 '20 at 09:59
  • 1
    @androiddeveloper `setFragmentResultListener` exists in fragment-ktx artifact. If you don't use it you can try to call `fragmentManager.setFragmentResultListener()` inside your fragment. – Ziem Jun 24 '20 at 06:18
  • Seems I was confusing between `androidx.navigation:navigation-fragment-ktx` and what you actually meant, which is `androidx.fragment:fragment-ktx`. Seems to work now. But you have some mistakes: It's not just `fragmentManager` (which is deprecated) that you need to use . You are supposed to use `parentFragmentManager`. In addition, inside the second Fragment you can't cast it to an Activity. You are supposed to use `findNavController().navigateUp()` directly. I've also noticed that the callback occurs right when I set the result, and not when I come back to the first Fragment. How come? – android developer Jun 24 '20 at 07:38
  • Yes, you are right in both cases. `parentFragmentManager` should be used as `fragmentManager` is deprecated (I can't edit my comment now). But I edited my answer and removed casting to `Activity` as you suggested. Using `findNavController()` is the correct way to do it. – Ziem Jun 24 '20 at 08:38
  • Documentation for `Fragment.setFragmentResultListener` says: ```Once this Fragment is at least in the androidx.lifecycle.Lifecycle.State.STARTED state, any results set by setFragmentResult using the same requestKey will be delivered to the FragmentResultListener.onFragmentResult callback.``` and I can confirm this behavior. – Ziem Jun 24 '20 at 08:39
  • But how could it be on "STARTED" state, if the current Fragment is the second one? Try for example to set the value, but delay the call to `findNavController().navigateUp()` (or not call it at all). And use logs to print when the callback is called. – android developer Jun 24 '20 at 11:18
  • It triggers `onFragmentResult` callback when I go back to the first `Fragment`, and it happens after `onStart` of that first `Fragment`. You start with `FragmentA` -> `FragmentB` -> `setFragmentResult` -> you press back -> `FragmentB` is closed -> `FragmentA` `onStart` is called -> and then `onFragmentResult` is triggered – Ziem Jun 24 '20 at 11:30
22

There are a couple of alternatives to the shared view model.

  1. fun navigateBackWithResult(result: Bundle) as explained here https://medium.com/google-developer-experts/using-navigation-architecture-component-in-a-large-banking-app-ac84936a42c2

  2. Create a callback.

ResultCallback.kt

interface ResultCallback : Serializable {
    fun setResult(result: Result)
}

Pass this callback as an argument (note it has to implement Serializable and the interface needs to be declared in its own file.)

<argument android:name="callback"
                  app:argType="com.yourpackage.to.ResultCallback"/>

Make framgent A implement ResultCallback, fragment B by will get the arguments and pass the data back through them, args.callback.setResult(x)

AntPachon
  • 1,152
  • 12
  • 14
  • Thanks! That article is pure gold. – Andre Romano May 20 '19 at 17:08
  • Thanks for the amazing reference! – Adam Oct 03 '19 at 16:01
  • This solution does not work for me. When I navigate from Fragment A(Caller) to FragmentB(Receiver). FragmentA instance gets destroyed. FragmentB has a reference to an old dead FragmentA. – Pablo Valdes Oct 24 '19 at 15:34
  • It works, but is it a good practice? I couldn't find any Google recommendation regards that – G_comp Nov 05 '19 at 14:10
  • 2
    This was working great for me and I thought it was the best solution, until the app was backgrounded while on the target fragment and it crashed with `NotSerializableException`. – Pilot_51 Nov 18 '19 at 22:09
  • You have to be extremely careful with the Serializable callback approach - it's extremely easy to accidentally make your callback not Serializable, and you'll only be alerted at runtime, when your component is recreated. This is explained in detail here: https://medium.com/@lukeneedham/listeners-in-dialogfragments-be636bd7f480 – Luke Needham Oct 30 '20 at 09:41
20

It looks like there isn't equivalent for startActivityForResult in Navigation Component right now. But if you're using LiveData and ViewModel you may be interested in this article. Author is using activity scoped ViewModel and LiveData to achieve this for fragments.

LaVepe
  • 1,137
  • 1
  • 8
  • 12
19

Recently (in androidx-navigation-2.3.0-alpha02 ) Google was released a correct way for achieve this behaviour with fragments.

In short: (from release note)

If Fragment A needs a result from Fragment B..

A should get the savedStateHandle from the currentBackStackEntry, call getLiveData providing a key and observe the result.

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

B should get the savedStateHandle from the previousBackStackEntry, and set the result with the same key as the LiveData in A

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

Related documentation

Jonas Schmid
  • 5,360
  • 6
  • 38
  • 60
Juis Kel
  • 361
  • 4
  • 8
  • I am getting null on my textview inside Fragment A observer. – Sagar Maiyad Mar 31 '20 at 11:10
  • 3
    What if I have an `Acitivity A ` that needs a result from `Activity B` ? I believe this wouldn't work since the `navController` is different in the two activities ? – David Seroussi Apr 07 '20 at 11:09
  • 1
    David, For the activity we are going to use the old fashion way - startActivityForResult mechanism – Juis Kel Apr 09 '20 at 16:54
  • 1
    is this still work? i cannot access `currentBackStackEntry` and `savedStateHandle` – Pavel Poley Apr 21 '20 at 09:33
  • on startActivityForResult, we have a request ID. What do we have here? Doesn't seem like we can differentiate between fragments requests this way. And "Type" is the type of the value of the key, right? But what if we handle multiple fragments that we start for result? Is there any working Github sample that uses this code? In addition, I've tried to use this, and it seems the callback is called twice: once when it's set, and another time when you get back to the first fragment. How come? – android developer Jun 23 '20 at 10:01
  • works for me, but don't forget to navigate back after setting the result `findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result)` `findNavController().navigateUp()` – ilham suaib Feb 06 '23 at 15:08
1

There is another alternative workaround. You can use another navigation action from your modal back to screen1, instead of using popBackStack(). On that action you can send whatever data you like to screen one. Use this strategy to make sure the modal screen isn't then kept in the navigation back stack: https://stackoverflow.com/a/54015319/4672107.

The only issue I see with this strategy is that pressing the back button will not send any data back, however most use cases require navigation after a specific user action and and in those situations, this workaround will work.

Carson Holzheimer
  • 2,890
  • 25
  • 36
0

To get the caller fragment, use something like fragmentManager.putFragment(args, TargetFragment.EXTRA_CALLER, this), and then in the target fragment get the caller fragment using

if (args.containsKey(EXTRA_CALLER)) {
    caller = fragmentManager?.getFragment(args, EXTRA_CALLER)
    if (caller != null) {
        if (caller is ResultCallback) {
            this.callback = caller
        }
    }
}
Pnemonic
  • 1,685
  • 17
  • 17