Manually clearing an Android ViewModel?

后端 未结 12 1005
独厮守ぢ
独厮守ぢ 2020-12-04 18:50

Edit: This question is a bit out of date now that Google has given us the ability to scope ViewModel to navigation graphs. The better approach (rather

相关标签:
12条回答
  • 2020-12-04 19:33

    If you check the code here you'll find out, that you can get the ViewModelStore from a ViewModelStoreOwner and Fragment, FragmentActivity for example implements, that interface.

    Soo from there you could just call viewModelStore.clear(), which as the documentation says:

     /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
    

    N.B.: This will clear all the available ViewModels for the specific LifeCycleOwner, this does not allow you to clear one specific ViewModel.

    0 讨论(0)
  • 2020-12-04 19:35

    As it was pointed out it is not possible to clear an individual ViewModel of a ViewModelStore using the architecture components API. One possible solution to this issue is having a per-ViewModel stores that can be safely cleared when necessary:

    class MainActivity : AppCompatActivity() {
    
    val individualModelStores = HashMap<KClass<out ViewModel>, ViewModelStore>()
    
    inline fun <reified VIEWMODEL : ViewModel> getSharedViewModel(): VIEWMODEL {
        val factory = object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                //Put your existing ViewModel instantiation code here,
                //e.g., dependency injection or a factory you're using
                //For the simplicity of example let's assume
                //that your ViewModel doesn't take any arguments
                return modelClass.newInstance()
            }
        }
    
        val viewModelStore = this@MainActivity.getIndividualViewModelStore<VIEWMODEL>()
        return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
    }
    
        val viewModelStore = this@MainActivity.getIndividualViewModelStore<VIEWMODEL>()
        return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
    }
    
    inline fun <reified VIEWMODEL : ViewModel> getIndividualViewModelStore(): ViewModelStore {
        val viewModelKey = VIEWMODEL::class
        var viewModelStore = individualModelStores[viewModelKey]
        return if (viewModelStore != null) {
            viewModelStore
        } else {
            viewModelStore = ViewModelStore()
            individualModelStores[viewModelKey] = viewModelStore
            return viewModelStore
        }
    }
    
    inline fun <reified VIEWMODEL : ViewModel> clearIndividualViewModelStore() {
        val viewModelKey = VIEWMODEL::class
        individualModelStores[viewModelKey]?.clear()
        individualModelStores.remove(viewModelKey)
    }
    

    }

    Use getSharedViewModel() to obtain an instance of ViewModel which is bound to the Activity's lifecycle:

    val yourViewModel : YourViewModel = (requireActivity() as MainActivity).getSharedViewModel(/*There could be some arguments in case of a more complex ViewModelProvider.Factory implementation*/)
    

    Later, when it's the time to dispose the shared ViewModel, use clearIndividualViewModelStore<>():

    (requireActivity() as MainActivity).clearIndividualViewModelStore<YourViewModel>()
    

    In some cases you would want to clear the ViewModel as soon as possible if it's not needed anymore (e.g., in case of it containing some sensitive user data like username or password). Here's a way of logging the state of individualModelStores upon every fragment switching to help you keep track of shared ViewModels:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        if (BuildConfig.DEBUG) {
            navController.addOnDestinationChangedListener { _, _, _ ->
                if (individualModelStores.isNotEmpty()) {
                    val tag = this@MainActivity.javaClass.simpleName
                    Log.w(
                            tag,
                            "Don't forget to clear the shared ViewModelStores if they are not needed anymore."
                    )
                    Log.w(
                            tag,
                            "Currently there are ${individualModelStores.keys.size} ViewModelStores bound to ${this@MainActivity.javaClass.simpleName}:"
                    )
                    for ((index, viewModelClass) in individualModelStores.keys.withIndex()) {
                        Log.w(
                                tag,
                                "${index + 1}) $viewModelClass\n"
                        )
                    }
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-04 19:35

    Typically you don't clear the ViewModel manually, because it is handled automatically. If you feel the need to clear your ViewModel manually, you're probably doing too much in that ViewModel...

    There's nothing wrong with using multiple viewmodels. First one could be scoped to the Activity while another one could be scoped to the fragment.

    Try to use the Activity scoped Viewmodel only for things that need to be shared. And put as many things as possible in the Fragment Scoped Viewmodel. The Fragment scoped viewmodel will be cleared when the fragment is destroyed. Reducing the overall memory footprint.

    0 讨论(0)
  • 2020-12-04 19:37

    As OP and Archie said, Google has given us the ability to scope ViewModel to navigation graphs. I will add how to do it here if you are using the navigation component already.

    You can select all the fragments that needs to be grouped together inside the nav graph and right-click->move to nested graph->new graph

    now this will move the selected fragments to a nested graph inside the main nav graph like this:

    <navigation app:startDestination="@id/homeFragment" ...>
        <fragment android:id="@+id/homeFragment" .../>
        <fragment android:id="@+id/productListFragment" .../>
        <fragment android:id="@+id/productFragment" .../>
        <fragment android:id="@+id/bargainFragment" .../>
    
        <navigation 
            android:id="@+id/checkout_graph" 
            app:startDestination="@id/cartFragment">
    
            <fragment android:id="@+id/orderSummaryFragment".../>
            <fragment android:id="@+id/addressFragment" .../>
            <fragment android:id="@+id/paymentFragment" .../>
            <fragment android:id="@+id/cartFragment" .../>
    
        </navigation>
    
    </navigation>
    

    Now, inside the fragments when you initialise the viewmodel do this

    val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
    

    if you need to pass the viewmodel factory(may be for injecting the viewmodel) you can do it like this:

    val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }
    

    Make sure its R.id.checkout_graph and not R.navigation.checkout_graph

    For some reason creating the nav graph and using include to nest it inside the main nav graph was not working for me. Probably is a bug.

    Source: https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e

    Thanks, OP and @Archie for pointing me in the right direction.

    0 讨论(0)
  • 2020-12-04 19:41

    I think I have a better solution.

    As stated by @Nagy Robi, you could clear the ViewModel by call viewModelStore.clear(). The problem with this is that it will clear ALL the view model scoped within this ViewModelStore. In other words, you won't have control of which ViewModel to clear.

    But according to @mikehc here. We could actually create our very own ViewModelStore instead. This will allow us granular control to what scope the ViewModel have to exist.

    Note: I have not seen anyone do this approach but I hope this is a valid one. This will be a really good way to control scopes in a Single Activity Application.

    Please give some feedbacks on this approach. Anything will be appreciated.

    Update:

    Since Navigation Component v2.1.0-alpha02, ViewModels could now be scoped to a flow. The downside to this is that you have to implement Navigation Component to your project and also you have no granualar control to the scope of your ViewModel. But this seems to be a better thing.

    0 讨论(0)
  • 2020-12-04 19:41

    Im just writing library to address this problem: scoped-vm, feel free to check it out and I will highly appreciate any feedback. Under the hood, it uses the approach @Archie mentioned - it maintains separate ViewModelStore per scope. But it goes one step further and clears ViewModelStore itself as soon as the last fragment that requested viewmodel from that scope destroys.

    I should say that currently whole viewmodel management (and this lib particularly) is affected with a serious bug with the backstack, hopefully it will be fixed.

    Summary:

    • If you care about ViewModel.onCleared() not being called, the best way (for now) is to clear it yourself. Because of that bug, you have no guaranty that viewmodel of a fragment will ever be cleared.
    • If you just worry about leaked ViewModel - do not worry, they will be garbage collected as any other non-referenced objects. Feel free to use my lib for fine-grained scoping, if it suits your needs.
    0 讨论(0)
提交回复
热议问题