20

I'm showing a dialog inside a fragment using childFragmentManager or within an Activity using the supportFragmentManager, in the process I would like to set the target fragment, like this:

val textSearchDialog = TextSearchDialogFragment.newInstance()
textSearchDialog.setTargetFragment(PlaceSearchFragment@this, 0)

But when running that code I get the error:

java.lang.IllegalStateException: Fragment TextSearchDialogFragment{b7fce67 #0 0} declared target fragment PlaceSearchFragment{f87414 #0 id=0x7f080078} that does not belong to this FragmentManager!

I don't know how to access the FragmentManager the navigation components are using to manage the showing of the fragment, is there a solution for this?

Eury Pérez Beltré
  • 2,017
  • 20
  • 28

4 Answers4

18

Update: as part of Navigation 2.3.0, Navigation adds explicit support for returning a result with a specific section on returning a result from a Dialog destination as an alternative to using a shared ViewModel.

Previous answer:

The recommended pattern for communicating between Fragments with the Navigation Architecture Components is via a shared ViewModel - a ViewModel that lives at the Activity level achieved by retrieving the ViewModel using ViewModelProvider(getActivity())

As per the documentation, this offers a number of benefits:

  • The activity does not need to do anything, or know anything about this communication.
  • Fragments don't need to know about each other besides the SharedViewModel contract. If one of the fragments disappears, the other one keeps working as usual.
  • Each fragment has its own lifecycle, and is not affected by the lifecycle of the other one. If one fragment replaces the other one, the UI continues to work without any problems.

You can also share ViewModels at a smaller scope than your whole activity by using a navigation graph scoped ViewModel.

ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • Great, later on thinked a little bit about it. Thanks for pointing that out. – Eury Pérez Beltré Jun 08 '18 at 02:45
  • 11
    I think this is an incorrect answer. No way you will want to create a shared ViewModel which lives for the entire lifecycle of a parent Activity just to communicate with your dialog. In fact, Navigation framework lacks of adding target fragments. – konata Jan 23 '19 at 15:33
  • 9
    Shared ViewModel doesn't sound right, event if Google recommends it. It looks similar to returning value from function through a global variable. I am achieving expected flow using `newFragment.setTargetFragment(this)` when navigating forward to detail/edit fragment, then calling `getTargetFragment().onActivityResult(result, getTargetRequestCode())` prior to navigating back (eg. fragmentManger.popBackstack()). One of the benefits is that Android handles cases when one of the fragments is not there automagically. I haven't found way of achieving such a flow with Navigation Component though. – jskierbi Mar 30 '19 at 23:11
  • 5
    @jskierbi - of course, that problem has considerable issues - your target Fragment is *not* started and therefore it is unsafe to do any operations there, you've tightly coupled your two Fragments together, and you're forced to use an Intent (and Parcelable objects) when really any object would do. Of course, it isn't perfect, so I'd recommend starring the [existing feature request](https://issuetracker.google.com/issues/79672220) for a native `navigateForResult()` type of API. – ianhanniballake Mar 31 '19 at 00:20
  • 1
    @ianhanniballake what happens to this sharedViewModel when both of the sharing fragments are destroyed ? Will it be GCed or just stay there till the activity is in memory ? – rd7773 Jan 22 '20 at 12:12
  • if you use getActivity() or requireActivity() all the data will live untill your activity finishes, this means that poping fragments will retain the data at the sharedviewmodel (if you dont update it in onDestroy() of the poping fragment), since the instance is within the lifecycle of the Activity, not the lifecycle of the Fragment @rd7773 – Gastón Saillén Mar 21 '20 at 18:11
  • A shared viewmodel does work, and its a legit use of viewmodel among fragments (and is recommended), but it's not a great solution for DialogFragments, because of their transient nature and the transient nature fo the data they handle. – Brill Pappin Jun 16 '20 at 13:29
  • You don't need to use a shared ViewModel. It is perfectly possible to set targetFragment with the Navigation component: see https://stackoverflow.com/a/64995726/6007104 – Luke Needham Nov 24 '20 at 22:25
6

To elaborate on the accepted answer:

(1) Create a shared view model that would be used to share data between fragments within that Activity.

public class SharedViewModel extends ViewModel {

    private final MutableLiveData<Double> aDouble = new MutableLiveData<>();

    public void setDouble(Double aDouble) {
        this.aDouble.setValue(aDouble);
    }

    public LiveData<Double> getDouble() {
        return aDouble;
    }
}

(2) Store the data you would like to access in the view model. Note the scope of the view model (getActivity).

SharedViewModel svm =ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
svm.setDouble(someDouble);

(3) Let the fragment implement the dialog's callback interface and load the dialog without setting a target fragment.

fragment.setOnDialogSubmitListener(this);
fragment.show(getActivity().getSupportFragmentManager(), TAG);

(4) Inside the dialog retrieve the data.

SharedViewModel svm =ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
svm.getDouble().observe(this, new Observer<Double>() {
    @Override
    public void onChanged(Double aDouble) {
        // do what ever with aDouble
    }
}); 
Jeffrey
  • 1,998
  • 1
  • 25
  • 22
4

With viewmodel and fragment ktx, you can host a shared viewmodel between a parent fragment and a child fragment, so instead of having your activity contain the instance of the viewmodel and storing the data until that activity finishes, you can store the viewmodel within the parent fragment, doing so, when you pop the fragment that instantiated the viewmodel, the viewmodel will be cleared

Imports

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
 implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'

ParentFragment (SharedViewModel host)

class ParentFragment:Fragment() {
 private val model: SharedViewModel by viewModels()
}

ChildFragment

class ChildFragment:Fragment(){
private val model: SharedViewModel by viewModels ({requireParentFragment()})
}

So, doing this will host the sharedviewmodel in the parent fragment, and the child fragment depending on that parent fragment will have access to that same instance of the SharedViewModel and when you pop (aka destroying the fragment) , your onCleared() method will fire at your viewmodel and that shareviewmodel will be cleared, and also all it's data.

This way, you don't have your MainActivity to contain all the data that fragments share, and you don't need to clear that data each time you leave a fragment that uses the SharedViewModel

Now in alpha, you can pass data between navigations using also a viewmodel that will save the data between navigations, lets say you want to share data between Fragment B and fragment A, now you can do it simply with two lines

https://developer.android.com/guide/navigation/navigation-programmatic#returning_a_result

Gastón Saillén
  • 12,319
  • 5
  • 67
  • 77
  • 2
    Way better answer than the accepted one. A SharedViewModel scoped to the activity is definitely not the way to go if you want to share data between two fragments "once". – reVerse Apr 24 '20 at 16:55
0

None of the existing answers actually answer your question - how can we set target fragment of a dialog when using navigation components?

It turns out we don't need to use the (frankly horrible) pattern of the shared ViewModel. It's actually quite easy to set target fragment with the Navigation Component, once you know how.

I've written a whole article on it, which you can read here:

https://lukeneedham.medium.com/using-targetfragment-with-jetpack-navigation-component-9c4302e8c062

You can also view the Gist here:

https://gist.github.com/LukeNeedham/83f0bdaa8d56d03d11f727967eb327f2

They key is a custom FragmentFactory:

fun FragmentManager.autoTarget() {
    fragmentFactory = ChildManagerFragmentFactory(this)
}

class ChildManagerFragmentFactory(
    private val fragmentManager: FragmentManager
) : AutoTargetFragmentFactory() {
    override fun getCurrentFragment() =
        fragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.firstOrNull()
}

abstract class AutoTargetFragmentFactory : FragmentFactory() {
    abstract fun getCurrentFragment(): Fragment?

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val fragment = super.instantiate(classLoader, className)
        val currentFragment = getCurrentFragment()
        fragment.setTargetFragment(currentFragment, REQUEST_CODE)
        return fragment
    }

    companion object {
        const val REQUEST_CODE = 0
    }
}

And then simply use like so:

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager.autoTarget()
    }
}
Luke Needham
  • 3,373
  • 1
  • 24
  • 41
  • 1
    Note that this approach is fundamentally flawed due to the batched nature of FragmentManager updates. For example, an [action with `popUpTo`](https://developer.android.com/guide/navigation/navigation-navigate#pop) will cause your `getCurrentFragment()` to return the about to be popped fragment, thus causing your new fragment to hold a hard reference to a fragment that will actually be destroyed before the new fragment goes through any lifecycle transitions, instantly creating a memory leak. – ianhanniballake Nov 25 '20 at 12:03
  • Also keep in mind that `setTargetFragment()` is itself [deprecated](https://developer.android.com/reference/androidx/fragment/app/Fragment#setTargetFragment(androidx.fragment.app.Fragment,%20int)), with replacements at both the [Navigation level](https://developer.android.com/guide/navigation/navigation-programmatic#returning_a_result) and [Fragment level](https://developer.android.com/guide/fragments/communicate#fragment-result) that, unlike `setTargetFragment()`, will work when using [upcoming features](https://youtu.be/RS1IACnZLy4?t=726) such as multiple back stacks and single lifecycle. – ianhanniballake Nov 25 '20 at 12:07
  • Hi Ian, just want to say I'm a huge fan! Thanks for pointing out this leak which I had overlooked. I have to say, I feel like these replacements are somewhat of a step backwards: I use the Jetpack Navigation Component so I no longer have to worry about bundles and keys for passing arguments, but apparently I do have to worry about them when returning results. The huge advantage of targetFragment is that it allowed us to implement interfaces, which could be used to pass results in a type-safe way, unlike the Bundle-based replacements – Luke Needham Nov 25 '20 at 15:21
  • Is there any plan from your side to implement a safe-args-esque, type-safe wrapper for passing results back, in the same way as we can currently pass arguments forwards @ianhanniballake ? – Luke Needham Nov 26 '20 at 09:56