Android Navigation Dialog Fragment callback

Viewed 2125

I have DialogFragment in my project(MVVM, Jetpack navigation) that called from different places and represents signature canvas. Related part in navigation:

 <dialog
        android:id="@+id/signPadDialogFragment"
        android:name="com.ui.signpad.SignPadDialogFragment"
        android:label="SignPadDialogFragment" />

 <fragment
        android:id="@+id/loginFragment"
        android:name="com.ui.login.LoginFragment"
        android:label="@string/login_label"
        tools:layout="@layout/login_fragment">

        <action
            android:id="@+id/action_loginFragment_to_currentJobsFragment"
            app:destination="@id/currentJobsFragment" />
        <action
            android:id="@+id/action_loginFragment_to_signPadDialogFragment"
            app:destination="@id/signPadDialogFragment" />

<fragment
        android:id="@+id/jobDetailFragment"
        android:name="com.ui.jobdetails.JobDetailFragment"
        android:label="job_detail_fragment"
        tools:layout="@layout/job_detail_fragment" >
        <action
            android:id="@+id/action_jobDetailFragment_to_signPadDialogFragment"
            app:destination="@id/signPadDialogFragment" />
    </fragment>

and navigate action:

 mainActivityViewModel.repository.navigationCommands.observe(this, Observer { navEvent ->
            navEvent.getContentIfNotHandled()?.let {
                navController.navigate(it as NavDirections)
            }

        })

So, my question is: what is the right way to handle callbacks using Jetpack navigation and MVVM? I see two possible solution and related questions:

I can pass data to ViewModel -> Repository from dialog fragment( and in this case: how to differ action that started dialog inside dialog scope?)

Or get a callback in MainActivity(How?)

Thanks in advance

1 Answers

Due to the limitations of NavController API, it can only be discovered from element that has android context present. That means your prime options are:

  1. AndroidViewModel: I would not recommend it as it is very easy to get carried away with context here, which will lead to memory leaks if you don't know what you are doing.
  2. Activity: Handle the navigation in the Activity. Single Activity architecture would complicate matters because you'd have to cramp all the navigation logic here.
  3. Fragment: Handle the navigation for each fragment in its scope. This is way better but there is an even better solution below.
  4. ViewModel: Handle each fragment's navigation in a viewModel scoped to it. (Personal preference).

Using ViewModel in Jetpack Navigation Component

Technically navigation login will still reside in Fragment, there is no escaping that unless navigation API changes, however we can delegate major part to the ViewModel as follow:

  1. ViewModel will expose a SingleLiveEvent encapsulating a NavDirection in it. SingleLiveEvent is a Live Data that only gets triggered once, which is what we want when it comes to navigation. There is a great blogpost by Jose Alcérreca on that:

    https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

  2. Fragment will observe this SingleLiveEvent and will use that NavDirection to carry out a navigation transaction.

ViewModel exposing SingleLiveEvent:

open class BaseViewModel : ViewModel() {
  /**
   * Navigation Component API allows working with NavController in the Following:
   * 1.) Fragment
   * 2.) Activity
   * 3.) View
   *
   * In order to delegate the navigation logic to a viewModel and allow fragment
   * or an activity to communicate with viewModel, we expose navigationCommands
   * as LiveData that contains Event<NavigationCommand> value.
   *
   * Event<T> is simply a wrapper class that will only expose T if it has not
   * already been accessed with the help of a Boolean flag.
   *
   * NavigationCommand is a Sealed class which creates a navigation hierarchy
   * where child classes can take NavDirections as properties. We will observe the
   * value of NavigationCommand in the fragment and pull the NavDirections there.
   */
  private val _navigationCommands = MutableLiveData<Event<NavigationCommand>>()
  val navigationCommands: LiveData<Event<NavigationCommand>>
    get() = _navigationCommands

  fun navigate(directions: NavDirections) {
    _navigationCommands.postValue(Event(NavigationCommand.To(directions)))
  }
}

Fragment observing this SingleLiveEvent from ViewModel:

  private fun setupNavigation() {
    viewModel.navigationCommands.observe(viewLifecycleOwner, Observer {
      val navigationCommand = it.getContentIfNotHandled()

      when (navigationCommand) {
        is NavigationCommand.To -> { findNavController().navigate(navigationCommand.directions) }
      }
    })
  }

You can make use of BaseFragment and BaseViewModels to follow DRY, but always remember that anything that has Base as a prefix will turn into a code smell soon, so keep them as concise as possible.

Related