Dagger-Android: ViewModel not destroyed when fragment destroyed - android

in my project I am using Dagger2 to inject ViewModels into fragments.
override val viewModel: AllStockListTabViewModel by viewModels ({this}, {viewModelFactory})
To briefly explain my situation, I have a fragment that uses fragment state adapter which contains two fragments. For convinience, I'll call the parent fragment fragment A and child fragments in fragment state adapter fragment B and fragment C.
Typically, when testing the app user spends time in fragment B that contains a recyclerview. When user taps one of the items, it leads to a different fragment with some detailed information. When user enters that detail fragment, fragment B holding that item goes through onPause() and onStop(). At the same time, onStop() is called in fragment C.
The point is, if user spends enough time in fragment B (contained by fragment A), fragment C is destroyed and this is not by surprise because I know that is intended by fragment state adapter. It is supposed to get rid of some fragments when not visible.
My problem is that when fragment C gets destroyed, viewmodel associated with it does not get destroyed. This is bad because now when user goes to fragment C, which still has reference to old viewmodel, app doesn't supply any data to the fragment because when onDestroy() is called, viewmodel of fragment C is cleared and thus viewmodelscope.launch is not working.
I also thought of not using viewmodelscope (use coroutinescope instead) but that is not the issue. What I am curious and eager to know is why viewmodel of fragment C, scoped to lifecycle of fragment C is not destroyed.(I want to get rid of old viewmodel at the demise of fragment C and get new viewmodel instance)
Please understand my clumsy wording and my lack of knowledge that might give out some confusion. I am new to dagger. Please see my code below for better understanding.
AppComponent.kt
#Singleton
#Component(
modules = [
AndroidSupportInjectionModule::class,
ActivityBindingModule::class,
RepositoryModule::class,
DataSourceModule::class,
ServiceModule::class,
DaoModule::class,
ViewModelModule::class,
]
)
ViewModelModule.kt
#MapKey
#Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
annotation class ViewModelKey(val value: KClass<out ViewModel>)
#Module
abstract class ViewModelModule {
#Binds
#IntoMap
#ViewModelKey(AllStockListTabViewModel::class)
abstract fun bindAllStockListTabViewModel(allStockListTabViewModel: AllStockListTabViewModel): ViewModel
}
ViewModelFactory
#Singleton
class ViewModelFactory #Inject constructor(
private val viewModelMap: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return viewModelMap[modelClass]?.get() as T
}
}
Fragment
class AllStockListTabFragment #Inject constructor() :
ViewModelFragment<FragmentAllStockListBinding>(R.layout.fragment_all_stock_list) {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override val viewModel: AllStockListTabViewModel by viewModels ({this}, {viewModelFactory})
}
Adapter
tradingTabAdapter = TradingTabAdapter(
this.childFragmentManager,
this.lifecycle,
tradingStateTabFragment,
allStockListTabFragment
)
class TradingTabAdapter #Inject constructor(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
private val tradingStateTabFragment: TradingStateTabFragment,
private val allStockListTabFragment: AllStockListTabFragment
) : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun createFragment(position: Int): Fragment =
when (position) {
0 -> tradingStateTabFragment
else -> allStockListTabFragment
}
override fun getItemCount(): Int = 2
}
SubComponent
#FragmentScope
#Subcomponent(
modules = [
TradingTabBindingModule::class,
TradingTabModule::class,
EventModule::class,
UseCaseModule::class
]
)
AdapterModule
#Module
class TradingTabModule {
#Provides
fun provideTradingTabAdapter(
fragment: TradingTabFragment,
allStockListTabFragment: AllStockListTabFragment,
tradingStateTabFragment: TradingStateTabFragment
) = TradingTabAdapter(
fragment.childFragmentManager,
fragment.lifecycle,
tradingStateTabFragment,
allStockListTabFragment
)
I found that create method of ViewModelFactory is not called when fragment C is destroyed and created again. I think this is because I am using lazy initialization of viewmodel and that is how ViewModelLazy works. It caches viewmodel and invokes factory's create method only when cache is null. I guess what's happening is old viewmodel of fragment C is still referencing the dead viewmodel(which survived viewModelStore.onclear). I put a log statement in the init block of viewmodel of fragment C and I can see that it is called only for the very frist time fragment C is created and never called again even when fragment C is destroyed and created again.
Thank you so much for your patience reading all this haha. So I need help from any expereienced Android gurus who might be able to give some insight.
My goal: make viewmodel destroyed and recreated with the lifecycle of fragment. I want to avoid memory leak due to unused zombie viewmodels.
Current situation: viewmodel never gets destroyed and reborn fragment still references old viewmodel and thus lazy initialisation keeps the cache of old viewmodel, not triggering create method of ViewModelFactory.
--Edit--
version of dagger im using
"com.google.dagger:dagger-android:2.37"

Since your ViewModel is tied to your Activity, it is not getting destroyed when Fragment is destroyed.
#ViewModelKey(MainActivityViewModel::class)
abstract fun bindMainActivityViewModel(mainActivityViewModel: MainActivityViewModel): ViewModel
You can check this answer which explains How to use ViewModel with Fragment.
How to use ViewModel in a fragment?

Related

How to share viewmodel between two fragments which belong to two different activities

I am trying to understand how can I use my same viewmodel across different fragments which even belongs to different activities.
So let's say I have Activity1 with Fragment A, Fragment B and Activity2 with Fragment C. How do I create a single instance of viewmodel that I can use across all these fragments.
I tried understanding shared viewmodel but seems like it is to be used if sharing data between fragments of a single activity and not multiple activities.
So basically I want to create a single instance of viewmodel across all the fragments? How can I achieve this functionality also keeping in mind the MVVM approach.
Any help would be appreciated. Thanks!
This is not supported. Google's recommendation is to put all your screens in a single Activity.
But, you can make an intermediate singleton class that each instance of the ViewModel uses.
Or maybe you could use a factory that treats it like a temporary singleton and does reference counting so it doesn't get cleared too early or hang onto the reference for too long. Untested example of what I mean:
private var viewModelInstance: MyViewModel? = null
private var refCount = 0
class MyViewModel: ViewModel() {
override fun onCleared() {
if (--refCount > 0) {
return
}
viewModelInstance = null
// Do typical onCleared cleanup here
}
}
class MyViewModelFactory: ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
require(modelClass.isAssignableFrom(MyViewModel::class.java)) { "Factory only supports MyViewModel" }
refCount++
viewModelInstance = viewModelInstance ?: MyViewModel()
return viewModelInstance as T
}
}
Shared viewmodel between multiple activities is not supported.
One way to achieve this using AndroidViewModel. You can create a ViewModel extending AndroidViewModel . This requires application instance. This viewmodel will be binded to application lifecycle and same instance will be available through out the lifecycle of the application. In one activity you can add data, and in other activity you can get updated data.
This will be acting something like singleton instance(But not exactly).
Addition to this, you can also use live data in AndroidViewModel if you use observer with activity/fragment lifecycle owner. So the observer will be live only till life cycle of fragment or activity.
ViewModel:
class AppViewModel constructor(private val mApplication: Application) :
AndroidViewModel(mApplication) {
//ViewModel Logic
}
Initializing Viewmodel:
You can initialize viewmodel like this in any fragment or activity.
val appViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(MyApp.getInstance())
.create(AppViewModel::class.java)
Application Class:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
mInstance = this
}
companion object {
lateinit var mInstance: CooperApp
#Synchronized
fun getInstance(): MyApp {
return mInstance
}
}
}
Also one more thing we can use is like initializing viewmodel in application class and create similar function to getInstance() which will return viewmodel instance and use it all over the app

SharedViewModel not clearing when poping fragment

I'm using a shared view model like here
But the problem is that when I clear my last fragment, I want to clear the viewmodel, or kill its instance, but somehow it survives when I leave the last fragment that uses it
How can I programatically clear this viewmodel ?
I use it like this
Fragment A
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.getTotal().observe(viewLifecycleOwner, Observer { cartTotal ->
total = cartTotal
})
}
From fragment B I sent the total
Fragment B
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.setTotal = 10
}
But when leaving Fragment A with that data (doing popBackStack since I'm using navigation components) it does not clear the viewmodel, instead when I open again my fragment , the data stills there
I suspect that the viewmodel is tied with my Container Activity and not the lifecycle of the fragments itself, so
How can I remove the instance or clear my viewmdel when I hit my last fragment ?
Thanks
If you want to get a ViewModel associated with a parent fragment, your inner fragment should follow the by viewModels JavaDoc and use:
val viewmodel: MYViewModel by viewmodels ({requireParentFragment()})
This says to use the parent Fragment as the owner of your ViewModel.
(The parent fragment would use by viewModels() as it is accessing its own ViewModels)
you can also clear viewModelStore manually after Fragment A destroyed.
something like this :
override fun onDetach() {
super.onDetach()
requireActivity().viewModelStore.clear()
}
then your viewModel instance will be cleared. for checking this work you can debug onCleared method of your viewModel.

Cannot Call Coroutine Scope Function in ViewModel

Here is my BaseViewModel
abstract class BaseViewModel : ViewModel(), CoroutineScope {
private val parentJob = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Main
override fun onCleared() {
super.onCleared()
parentJob.cancel()
}
}
Then I have LevelFragment with LevelViewModel which is inherited BaseViewModel. And in LevelViewModel I have fun load() = launch {} function.
What I'm trying to do is showing multiple LevelFragment using ViewPager. So I create PagerAdapter and inherit FragmentStatePagerAdapter. What the documentation said about FragmentStatePagerAdapter is When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. I'm not sure is the viewModel part of the saved state or not but when I go to the destroyed fragment and the fragment recreated I can't call load() function, it's not compile error or runtime crash, but when I try to put a breakpoint in the function it's not get called. I am still able to call another function that has no coroutine scope.
Also, I already try to use viewModelScope and the result still the same.
Is anyone else running into these issues too, or do you have an idea of what could be causing this?

Shared ViewModel across two/or more fragments and an activity using Dagger

Well, as I tried to summarise in the title, here is the details.
We have a relatively large application, that uses Dagger, in really not ideal ways, so we decided to start writing tests, and for that, I needed to expose dependencies for Mockito, hence, I faced an issue to start provide view models using a singleton factory, still applicable and there is tons of tutorials around that explains this.
We have across our app, a lot of features, that is implemented using a single activity, and a navigation component, that single activity sometimes have a created view model that we use to share data between the container activity and the fragments populated using the navigation editor.
What I couldn't figure out is the following, how can I use dagger to inject a shared view model, to return the same instance each time I invoke #Inject for a specific view model, I know it could be done through scopes maybe, but I couldn't figure it out, and I have an explanation that I need to be verified. (I will provide my code below)
I started by implementing my Singleton ViewModelFactory as follows:
#Singleton
class ViewModelFactory #Inject constructor(private val creators: Map<Class<out ViewModel>,
#JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
try {
#Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Then I created my ViewModelModule that provides the ViewModelFactory and the ViewModel as follows:
#Module
abstract class ViewModelFactoryModule {
#Binds
abstract fun bindsViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#EbMainScope
#ViewModelKey(EBMainContainerViewModel::class)
abstract fun bindEbMainViewModel(ebMainContainerViewModel: EBMainContainerViewModel): ViewModel
}
And before you ask, here is the scope implementation:
#Scope
#Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
#Retention(AnnotationRetention.RUNTIME)
annotation class EbMainScope
Last step, here is my activity/fragment injectors module:
#Module
abstract class ScreensBuildersModule {
#ContributesAndroidInjector
#EbMainScope
abstract fun contributeEbMainActivity(): EBMainActivity
#ContributesAndroidInjector
#EbMainScope
abstract fun contributeEbDashboardMainFragment(): EBDashboardMainFragment
}
Of course I wired everything in the AppComponent, and the app ran smoothly, with the catch, that there was two instances of EbMainContainerViewModel, despite me defining the scope.
My explanation was, I actually had two different providers rather than one, but I still cannot understand why, since I marked it as #Singleton.
Does someone has an explanation to this ? If more input is needed let me know guys.
I had the same problem, but fix it this way:
I use for example this code:
https://github.com/android/architecture-samples/tree/dagger-android
In my fragments (in which I wanted to use Shared ViewModel) I use
this (it helped me):
private val viewModel by viewModels<SearchViewModel>({ activity as MainActivity }) { viewModelFactory }
instead of this(as in sample):
private val viewModel by viewModels<SearchViewModel> { viewModelFactory }
Because the first argument is ownerProducer, so we create a ViewModel in the activity scope.
Okay then, here is practical guide I have managed to do, a solution I guess viable, and since #pratz9999 asked for a solution, here it is:
In order to instantiate a ViewModel, you would need a ViewModelProvider, which under the hood creates a ViewModelFactory, if you would depend on the above implementation, for each entry in the module (i.e #IntoMap call) a new provider will be instantiated (which is fines) but here goes the catch, it will create a new ViewModelFactory each time, take a look at the following:
/**
* Creates a {#link ViewModelProvider}, which retains ViewModels while a scope of given
* {#code fragment} is alive. More detailed explanation is in {#link ViewModel}.
* <p>
* It uses the given {#link Factory} to instantiate new ViewModels.
*
* #param fragment a fragment, in whose scope ViewModels should be retained
* #param factory a {#code Factory} to instantiate new ViewModels
* #return a ViewModelProvider instance
*/
#NonNull
#MainThread
public static ViewModelProvider of(#NonNull Fragment fragment, #Nullable Factory factory) {
Application application = checkApplication(checkActivity(fragment));
if (factory == null) {
factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}
return new ViewModelProvider(fragment.getViewModelStore(), factory);
}
My fault as I guessed after some research, that I did not inject the proper ViewModelFactory, so I ended up doing the following:
In my base fragment class, I injected a ViewModelFactory as follows:
/**
* Factory for injecting view models
*/
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
Then in a utility class, I had a method that returns a shared ViewModel as follows (Note the activity?.run this makes the view model instance binded to the holding activity, and thus having the shared scope concept):
fun <T: ViewModel> BaseNavFragmentWithDagger.getSharedViewModelWithParams(clazz: Class<T>): T =
activity?.run { ViewModelProviders.of(this, viewModelFactory).get(clazz) }
?: throw RuntimeException("You called the view model too early")
And finally for private ViewModels I went with this:
fun <T: ViewModel> BaseNavFragmentWithDagger.getPrivateViewModelWithParams(clazz: Class<T>): T =
ViewModelProviders.of(this, viewModelFactory).get(clazz)

Clearing sharedViewModel

I am using Koin for injecting viewModel into fragment. My app is single activity. I need that sharedViewModel only in servisFragment and partFragment.
I would like to clear that viewModel from Activity after navigation marked with red.
How can I do that?
Code for injecting viewModel
private val servisViewModel by sharedViewModel<ServisViewModel>()
Koin sharedViewModel
inline fun <reified T : ViewModel> Fragment.sharedViewModel(
name: String? = null,
noinline from: ViewModelStoreOwnerDefinition = { activity as
ViewModelStoreOwner },
noinline parameters: ParametersDefinition? = null
) = lazy { getSharedViewModel<T>(name, from, parameters) }
Thank you for any help.
if you need to clear all viewModels from that Fragment try this in your Fragment
viewModelStore.clear()
if you need to clear concrete ViewModel try this
getViewModelStore(ViewModelParameters(...)).clear()
If you are using koin to inject, in the onDestoy of the fragment you should use
requireActivity().viewModelStore.clear()
because viewModelStore directly from fragment will return none to clear
But the problem with this is that it will clear ALL the view model scoped within this ViewModelStore. So you won't have control of which ViewModel to clear.

Categories

Resources