20

I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?

class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"


 fun start(){
    if(dealerProfile.getOperatorId().isEmpty()){
        if(dealerProfile.isFirstTimeLaunch()){
            Log.d(TAG, "First Time Launch")
            showTour()
        }else{
            showCodeFragment()
            Log.d(TAG, "Show Code Fragment")

        }
    }
}

private fun showCodeFragment(){
    //??
}

private fun showTour(){
    //??
}

}

My Fragment

class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
    return inflater.inflate(R.layout.fragment_caption, container, false)
}


override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)

    viewModel.start()

}

}

I want to keep logic of navigation in ViewModel

Suleyman
  • 2,765
  • 2
  • 18
  • 31
Ekaterina Levchenko
  • 351
  • 1
  • 2
  • 15
  • 3
    The Navigation library is a primarily UI component, you need to keep the UI logic in the UI, therefore you need to perform navigation in the Fragment/Activity – Levi Moreira Jun 07 '18 at 12:09
  • I will have some backend loads there and based on them I will know where to go. There must be a way to do it – Ekaterina Levchenko Jun 07 '18 at 12:17
  • 2
    Then you need to propagate those options as state to the UI and decide there. The ViewModel shouldn't know anything about the view, so the way it was architectured it simply emits data to some view that will subscrbe to its data. Maybe you looking for an architecture where the ViewModel can actively change the View. That would be an MVP architecture where the presenter can actively change the view – Levi Moreira Jun 07 '18 at 12:21
  • Even though I tried to do that navigation in Fragment class, the error `FragmentManager is already executing transactions` occurred – Ekaterina Levchenko Jun 07 '18 at 12:28
  • I agree with everything @LeviAlbuquerque said. Haven't seen your code but this could be the [issue](https://issuetracker.google.com/u/1/issues/79632233) which will be fixed in alpha02 – Sander Jun 07 '18 at 12:37

3 Answers3

22

How can I navigate from ViewModel?

The answer is please don't. ViewModel is designed to store and manage UI-related data.

New Answer

In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.

But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?

So I check a sample from "android-architecture", and I found some interesting way how Google did it.

Please check here: todo-mvvm-databinding

As it turns out, they indeed drive UI from model. But how?

  1. They created an interface TasksNavigator that basically just a navigation interface.
  2. Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
  3. Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
Yosi Pramajaya
  • 3,895
  • 2
  • 12
  • 34
  • 6
    Why is the application's navigation state the responsibility of the View? The View's responsibility is to display data, not to know what screen to show next. – EpicPandaForce Jun 07 '18 at 17:51
  • 2
    I didn't say it's the responsibility of the View. But it is not responsibility of the ViewModel also. Navigating through ViewModel is never a good idea. – Yosi Pramajaya Jun 08 '18 at 01:10
  • I think you should at least explain why this is 'never a good idea'. – Neal Sanche Oct 01 '18 at 03:16
  • 17
    The ViewModel is the right place for navigation, it's just that in Android that becomes a problem because we need access to the view to perform this navigation. There is no good way to abstract navigation either. So, generally speaking, navigation can't easily be done from the VM on Android, but it can on other platforms. – Glaucus Oct 02 '18 at 16:59
  • I have edited the answers and hopefully you can understand. – Yosi Pramajaya Oct 03 '18 at 00:50
  • 1
    Just as a matter of interest, I like Yosi's current answer better. We have been using Cicerone and it has a Router object that provides access to perform navigation from the ViewModel, and this works quite well. I might have to see what it would take to integrate Cicerone with the Navigation Component at some point. – Neal Sanche Oct 10 '18 at 16:18
  • 4
    I might be wrong or late but doesn't that strategy pass a reference to the view to the viewmodel? that could lead to memory leaks – jack_the_beast Sep 22 '20 at 13:01
  • I believe this topic needs more discussion with Google. Microsoft Frameworks like Xamarin actively encourage navigation from the ViewModel. It is very useful for keeping code separate from the View, ie arguments being passed between screens which drive the UI. Why isn't this the same for native Android? – Chucky Jan 18 '22 at 14:51
  • Because Android is old and need to be retro-compatible, sadly. – Nino DELCEY Mar 02 '23 at 10:39
2

You can use an optional custom enum type and observe changes in your view:

enum class NavigationDestination {
    SHOW_TOUR, SHOW_CODE_FRAGMENT
}

class CaptionViewModel(app: Application) : AndroidViewModel(app) {
    private val dealerProfile = DealerProfile(getApplication())
    val TAG = "REGDEB"

    private val _destination = MutableLiveData<NavigationDestination?>(null)
    val destination: LiveData<NavigationDestination?> get() = _destination
    
    fun setDestinationToNull() {
        _destination.value = null
    }
    

    fun start(){
        if(dealerProfile.getOperatorId().isEmpty()){
            if(dealerProfile.isFirstTimeLaunch()){
                Log.d(TAG, "First Time Launch")
                _destination.value = NavigationDestination.SHOW_TOUR
            }else{
                _destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
                Log.d(TAG, "Show Code Fragment")

            }
        }
    }
}

And then in your view observe the viewModel destination variable:

viewModel.destination.observe(this, Observer { status ->
            if (status != null) {
                viewModel.setDestinationToNull()
                status?.let {
                    when (status) {
                        NavigationDestination.SHOW_TOUR -> {
                            // Navigate to your fragment
                        }
                        NavigationDestination.SHOW_CODE_FRAGMENT -> {
                            // Navigate to your fragment
                        }
                    }
                })
            }

If you only have one destination you can just use a Boolean rather than the enum.

alionthego
  • 8,508
  • 9
  • 52
  • 125
  • `viewModel.setDestinationToNull()` is an anti-pattern, your view shouldn't drive your ViewModel. Use an EventWrapper or a SingleLiveEvent instead to communicate an Event rather than a Data to your view. – Nino DELCEY Mar 02 '23 at 10:32
1

There are two ways I can recommend doing this.

  1. Use LiveData to communicate and tell the fragment to navigate.
  2. Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.
Rubin Yoo
  • 2,674
  • 2
  • 24
  • 32