Use the same instance of view model in multiple fragments using dagger2
Asked Answered
H

3

7

I am using only dagger2 (not dagger-android) in my project. It's working fine to inject the ViewModel using multibinding. But there's one problem with that previously without dagger2 I was using the same instance of viewmodel used in activity in multiple fragments (using fragment-ktx method activityViewModels()), but now since dagger2 is injecting the view model it's always gives the new instance (checked with hashCode in each fragment) of the viewmodel for each fragment, that's just breaks the communication between fragment using viewmodel.

The fragment & viewmodel code is as below:

class MyFragment: Fragment() {
    @Inject lateinit var chartViewModel: ChartViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)
        (activity?.application as MyApp).appComponent.inject(this)
    }

}

//-----ChartViewModel class-----

class ChartViewModel @Inject constructor(private val repository: ChartRepository) : BaseViewModel() {
   //live data code...
}

Here's the code for viewmodel dependency injection:

//-----ViewModelKey class-----

@MapKey
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

//-----ViewModelFactory class------

@Singleton
@Suppress("UNCHECKED_CAST")
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 {
        val creator = viewModelMap[modelClass] ?: viewModelMap.asIterable()
            .firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
        ?: throw IllegalArgumentException("Unknown ViewModel class $modelClass")

        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

//-----ViewModelModule class-----

@Module
abstract class ViewModelModule {
    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(ChartViewModel::class)
    abstract fun bindChartViewModel(chartViewModel: ChartViewModel): ViewModel
}

Is there any way to achieve the same instance of viewmodel for multiple fragment and also at the same time inject the view model in fragments. Also is there any need for the bindViewModelFactory method as it seems to have no effect on app even without this method.

One workaround could be to make a BaseFragment for fragments which shares the common viewmodel, but that will again include the boilerplate code and also I am not a great fan of BaseFragment/BaseActivity.

This is generated code for ChartViewModel which always create the newInstance of viewModel:

@SuppressWarnings({
    "unchecked",
    "rawtypes"
})
public final class ChartViewModel_Factory implements Factory<ChartViewModel> {
  private final Provider<ChartRepository> repositoryProvider;

  public ChartViewModel_Factory(Provider<ChartRepository> repositoryProvider) {
    this.repositoryProvider = repositoryProvider;
  }

  @Override
  public ChartViewModel get() {
    return newInstance(repositoryProvider.get());
  }

  public static ChartViewModel_Factory create(Provider<ChartRepository> repositoryProvider) {
    return new ChartViewModel_Factory(repositoryProvider);
  }

  public static ChartViewModel newInstance(ChartRepository repository) {
    return new ChartViewModel(repository);
  }
}
Hohenlinden answered 23/5, 2020 at 13:27 Comment(1)
Simply, why don't you try "activityViewModels"? You want 2 fragments have ability to access a same ViewModel instance, right?Glean
C
9

The problem is that when you inject the viewmodel like this

class MyFragment: Fragment() {
    @Inject lateinit var chartViewModel: ChartViewModel

dagger simply creates a new viewmodel instance. There is no viewmodel-fragment-lifecycle magic going on because this viewmodel is not in the viewmodelstore of the activity/fragment and is not being provided by the viewmodelfactory you created. Here, you can think of the viewmodel as any normal class really. As an example:

class MyFragment: Fragment() {
    @Inject lateinit var anything: AnyClass
}
class AnyClass @Inject constructor(private val repository: ChartRepository) {
   //live data code...
}

Your viewmodel is equivalent to this AnyClass because the viewmodel is not in the viewmodelstore and not scoped to the lifecycle of the fragment/activity.

Is there any way to achieve the same instance of viewmodel for multiple fragment and also at the same time inject the view model in fragments

No. Because of the reasons listed above.

Also is there any need for the bindViewModelFactory method as it seems to have no effect on app even without this method.

It does not have any effect because (I'm assuming that) you are not using the ViewModelFactory anywhere. Since it's not referenced anywhere, this dagger code for the viewmodelfactory is useless.

@Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

Here's what @binds is doing: 1 2

That's why removing it has no effect on the app.

So what is the solution? You need to inject the factory into the fragment/activity and get the instance of the viewmodel using the factory

class MyFragment: Fragment() {
    @Inject lateinit var viewModelFactory: ViewModelFactory

    private val vm: ChartViewModel by lazy {
        ViewModelProvider(X, YourViewModelFactory).get(ChartViewModel::class.java)
    }

What is X here? X is ViewModelStoreOwner. A ViewModelStoreOwner is something that has viewmodels under them. ViewModelStoreOwner is implemented by activity and fragment. So you have a few ways of creating a viewmodel:

  1. viewmodel in activity
ViewModelProvider(this, YourViewModelFactory)
  1. viewmodel in fragment
ViewModelProvider(this, YourViewModelFactory)
  1. viewmodel in fragment (B) scoped to a parent fragment (A) and shared across child fragments under A
ViewModelProvider(requireParentFragment(), YourViewModelFactory)
  1. viewmodel in fragment scoped to parent activity and shared across fragments under the activity
ViewModelProvider(requireActivity(), YourViewModelFactory)

One workaround could be to make a BaseFragment for fragments which shares the common viewmodel, but that will again include the boilerplate code and also I am not a great fan of BaseFragment/BaseActivity

Yes, this is indeed a bad idea. The solution is to use requireParentFragment() and requireActivity() to get the viewmodel instance. But you'll be writing the same in every fragment/activity that has a viewmodel. To avoid that you can abstract away this ViewModelProvider(x, factory) part in a base fragment/activity class and also inject the factory in the base classes, which will simplify your child fragment/activity code like this:

class MyFragment: BaseFragment() {

    private val vm: ChartViewModel by bindViewModel() // or bindParentFragmentViewModel() or bindActivityViewModel()
Chose answered 23/5, 2020 at 15:25 Comment(9)
Thanks @sonnet, worked like a charm. One thing to mention instead of initializing the viewmodel with ViewModelProvider I am using fragment-ktx function activityViewModels, which is more cleaner.Hohenlinden
Yes, that's another way to go which is equivalent to no.4. Also, if you use navigation component, then you can also use by navgraphviewmodels which is equivalent to no.3Chose
I manage to implement this with ViewModelProvider and lazy but when I use fragment-ktx and viewModels I cannot accomplice communication between Activity and Fragment @Chose Do you have any Idea . In activity : val viewModel : ShopViewModel by viewModels{ viewModelFactory } in Fragment : private val viewModel: ShopViewModel by viewModels{ requireActivity() viewModelFactory }Preparator
I believe there is an extension function called activityViewModels(). You can use that https://mcmap.net/q/1241759/-viewmodelproviders-is-not-working-inside-my-fragmentChose
I tried adding val viewModel: NewViewModelCheckIn by lazy { ViewModelProvider(this, viewModelFactory).get(NewViewModelCheckIn::class.java) } to all three of my fragments that share the viewmodel but it doesnt workChunky
If I set all three fragments to requireParentFragment() this works. however when I complete the usecase and start it again all the data from the previous time in the viewmodel is still there. So after the 3 fragments end this viewmodel isnt being destroyed it persists and keeps the data....is this supposed to happen?Chunky
@Chunky so let's say you've a parent fragment A and under A you have three other child fragments B, C and D. If you do requireParentFragment() in B, C and D, it means that the viewmodel is scoped to the lifecycle of fragment A which is the parent of B, C and D. Only when the parent fragment A is destroyed, the viewmodel is destroyed.Chose
Hi @denvercoder9, Thank you for above answer. It is really helpful. I learn new concept. In addition to above, while injecting ViewModel with Dagger2, my concern is that my ViewModel's onClear method is not getting called when its attached fragment is destroyed. How to resolve this. Please guide.Oxazine
@Oxazine probably because you're injecting the viewmodel into the fragment. Please see the answer again. If you don't bind the lifecycleOwner with the viewmodel then the viewmodel is not lifecycle aware.Chose
P
1

You can share ViewModel between fragments when instantiating if the fragments has the same parent activity

FragmentOne

class FragmentOne: Fragment() {

private lateinit var viewmodel: SharedViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    viewmodel= activity?.run {
        ViewModelProviders.of(this).get(SharedViewModel::class.java)
    } : throw Exception("Invalid Activity")
  }
}

FragmentTwo

class FragmentTwo: Fragment() {

private lateinit var viewmodel: SharedViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    viewmodel= activity?.run {
        ViewModelProviders.of(this).get(SharedViewModel::class.java)
    } ?: throw Exception("Invalid Activity")

 }
}
Pyrophosphate answered 23/5, 2020 at 14:24 Comment(4)
Thanks, but I want to inject the view model with dagger.Hohenlinden
oh,I found this I think that you can apply the same conceptPyrophosphate
Thanks! See also similar solution: blog.mindorks.com/….Slav
What if fragments belongs to different activities ?Mindexpanding
F
0

Add your ViewModel as PostListViewModel inside ViewModelModule:

@Singleton
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
}

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

@Module
abstract class ViewModelModule {

    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(PostListViewModel::class)
    internal abstract fun postListViewModel(viewModel: PostListViewModel): ViewModel

    //Add more ViewModels here
}

To end with, our activity will have ViewModelProvider.Factory injected and it will be passed to theprivate val viewModel: PostListViewModel by viewModels { viewModelFactory }

class PostListActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory
    private val viewModel: PostListViewModel by viewModels { viewModelFactory }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_post_list)
        getAppInjector().inject(this)
        viewModel.posts.observe(this, Observer(::updatePosts))
    }

    //...
}

For more check this post:Inject ViewModel with Dagger2 And Check github

Fyke answered 23/5, 2020 at 14:39 Comment(2)
This doesn't work, still getting the different instance of viewmodel, and also ViewModelProviders is deprecated. And if we can't inject the viewmodel with field injection then then it would be better to stop the DI at repository label in case of shared viewmodel and just use manual instantiation of repository in viewmodel and use viewModels or activityViewModels (from ktx library) to instantiate viewmodel in activity/fragments.Hohenlinden
yes, you are right. it's deprecated I edited the answer. use lazy injection. Check this link proandroiddev.com/… it will really help you @deepakkumarFyke

© 2022 - 2024 — McMap. All rights reserved.