2

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"

Austin
  • 63
  • 1
  • 7
  • Why your fragment has @Inject constructor, how do you create fragments in adapter and how you inject viewModelFactory into fragments? Add your Adapter to the question – IR42 Nov 25 '21 at 12:57
  • @IR42 hey, thanks for comment. I just added code for adapter. I don't really need Inject constructor but I added there following convention of the app I am building. Some fragments need Inject constructor for parameters. – Austin Nov 25 '21 at 13:04
  • remove fragments from adapter constructor and create them in createFragment on each call – IR42 Nov 25 '21 at 14:17
  • @IR42 Ok so I removed fragment parameters from constructor and created those fragments in createFragment but still out of luck. Thanks for suggestion. Why did you think that might cause the issue? – Austin Nov 25 '21 at 14:54

1 Answers1

0

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?

Mohit Ajwani
  • 1,328
  • 12
  • 24