rupinderjeet
rupinderjeet

Reputation: 2838

Should I use ViewModelScope independent of Fragment's lifecycle?

TL;DR

For one instance of a fragment, that is added/removed again and again, viewModelScope in its ViewModel only works until the first time fragment is popped. After this, viewModelScope becomes inactive. How can I reinitialize the viewModelScope when I add the fragment back? Or Should I look for a better implementation to keep it active?

Scenario:

I have used RxJava through ViewModel to retrieve data from network in an old project, and to send the updates to UI, I have used LiveData.

Following is how I initialised a typical ViewModel inside a fragment:

private val fruitsViewModel by viewModels<FruitsViewModel>()

Following is the above mentioned ViewModel:

class FruitsViewModel: ViewModel {

    private var category: String = FruitCategories.DEFAULT

    private val _fruits = MutableLiveData<List<Fruit>>()
    fun fruits(): LiveData<List<Fruit>> = _fruits

    fun updateCategory(newCategory: String) {
        this.category = newCategory
        fetchFruits()
    }

    fun fetchFruits() { ... }
}

In Fragment's onViewCreated(), I called fruitsViewModel.fetchFruits(). This worked great(without any visible problems).

Today, I switched RxJava specific code with coroutines to retrieve network data in my ViewModel. Something like this:

fun fetchFruits() {

    /* this.fruitsJob = */ 
    viewModelScope.launch {
        ...
    }
}

This works good too, even after I replace a fragment and come back to it. But, if I keep a fragment instance declared in Activity, and use it again, viewModelScope.launch {} doesn't work anymore. Something like this:

class FruitsActivity: AppCompatActivity() {

    private val fruitsFragment = FruitsFragment()

    private fun showFruitsFragment() {
        supportFragmentManager
            .beginTransaction()
            .replace(getFragmentContainerId(), fruitsFragment)
            .addToBackStack(fruitsFragment.tag)
            .commit()
    }

    private fun removeFragments() {
        val manager = supportFragmentManager
        if (manager.backStackEntryCount > 0) {

            val first: FragmentManager.BackStackEntry = manager.getBackStackEntryAt(0)
            manager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
        }
    }
} 

The reason for keeping the fragment instance is to load API data only once, and after that, it should just appear on the click of a button without any flicker/loading.

After I call removeFragments(), fruitsViewModel has already called its onCleared() for kept declaration of fruitsFragment. So, its viewModelScope becomes inactive, and if I add fruitsFragment back again by calling showFruitsFragment(), this will not retrieve network data anymore. This was not the case for my Rxjava implementation because I didn't use viewModelScope.

I think I can just fix the problem by linking fruitsViewModel to FruitsActivity's instance instead. But, I want to know if this can be considered as the best way.

Upvotes: 1

Views: 1385

Answers (1)

R&#243;bert Nagy
R&#243;bert Nagy

Reputation: 7642

Yes, you should scope your viewModel differently, in your case scoping fruitsViewModel to FruitsActivity makes complete sense.

I'm not sure how your RxJava code worked, without seeing the code I'm guessing, that it has something to do with it not being disposed correctly.

Also, even if you would create a scope, that outlives the viewModelScope (for ex: create and maintain a CoroutineScope in the activity) my main issue is that approach is not intuitive/natural, people will have a harder time checking how & where is that CoroutineScope maintained

Upvotes: 1

Related Questions