使用dagger2在多个片段中使用视图模型的相同实例 [英] Use the same instance of view model in multiple fragments using dagger2

查看:86
本文介绍了使用dagger2在多个片段中使用视图模型的相同实例的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在项目中仅使用dagger2(未使用dagger-android )。使用多重绑定注入ViewModel效果很好。但是以前没有dagger2的地方存在一个问题,我在多个片段中使用了活动中使用的同一viewmodel实例(使用fragment-ktx方法activityViewModels()),但是现在由于 dagger2注入了视图模型,因此它始终提供每个片段的视图模型的新实例(在每个片段中用hashCode检查),只是使用viewmodel中断了片段之间的通信。



片段& viewmodel代码如下:

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

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

}

// --- ChartViewModel类-----

类ChartViewModel @Inject构造函数(私有val存储库:ChartRepository) :BaseViewModel(){
//实时数据代码...
}

这是视图模型依赖项注入的代码:

  // ----- ViewModelKey类----- 

@MapKey
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION,AnnotationTarget.PROPERTY_GETTER,AnnotationTarget.PROPERTY_SETTER)
内部注释类ViewModelKey(val value:KClass< ; out ViewModel>)

// --- ViewModelFactory类------

@Singleton
@Suppress( UNCHECKED_CAST)
类ViewModelFactory
@Inject构造函数(
私有val viewModelMap:Map< Class< out ViewModel> ;, @JvmSuppressWildcards Provider< ViewModel>>
):ViewModelProvider.Factory {

覆盖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类-----

@Module
抽象类ViewModelModule {
@Binds
内部抽象乐趣bindViewModelFactory(factory:ViewModelFactory):ViewModelProvider.Factory

@Binds
@IntoMap
@ViewModelKey(ChartViewModel :: class)
抽象乐趣bindChartViewModel(chartViewModel:ChartViewModel):ViewModel
}

有没有办法为多个片段实现相同的viewmodel实例,
也不需要bindViewModelFactory方法,因为即使没有此方法,它对应用程序也没有影响。



一种解决方法可以是为具有共同视图模型的片段制作 BaseFragment ,但这又包括样板代码,而且我不是



这是为ChartViewModel生成的代码,该代码始终创建viewModel的newInstance:

  @SuppressWarnings({
unchecked,
rawtypes
})
公共最终类ChartViewModel_Factory实现Factory< ChartViewModel> {
私人最终Provider< ChartRepository> repositoryProvider;

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

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

public static ChartViewModel_Factory create(Provider< ChartRepository> repositoryProvider){
返回新的ChartViewModel_Factory(repositoryProvider);
}

公共静态ChartViewModel newInstance(ChartRepository存储库){
return new ChartViewModel(repository);
}
}


解决方案

问题是当您像这样注入视图模型时

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

dagger只会创建一个新的viewmodel实例。没有viewmodel-fragment-lifecycle魔术,因为此ViewModel不在活动/片段的ViewModel存储中,并且您创建的ViewModelFactory并未提供该ViewModel。在这里,您可以将viewmodel视为任何普通类。例如:

  class MyFragment:Fragment(){
@Inject lateinit var any:AnyClass
}
类AnyClass @Inject构造函数(私有val存储库:ChartRepository){
//实时数据代码...
}

您的视图模型与此 AnyClass 等效,因为该视图模型不在视图模型存储中,并且不属于该片段的生命周期/ activity。


有没有办法为多个片段实现相同的viewmodel实例,并且同时将视图模型注入片段中


否。由于上述原因。


也不需要bindViewModelFactory方法,因为即使没有该方法,它对应用程序也没有影响。

p>

它没有任何作用,因为(我假设是)您没有使用 ViewModelFactory 任何地方。由于没有在任何地方引用它,因此viewmodelfactory的此匕首代码无用。

  @Binds 
内部摘要fun bindViewModelFactory(factory:ViewModelFactory):ViewModelProvider.Factory

这是@binds在做什么: 1 2


这就是为什么删除它对应用程序没有影响的原因。 / p>

那么解决方案是什么?您需要将工厂注入片段/活动中,并使用工厂获取视图模型的实例

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

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

这里的 X 是什么? X是 ViewModelStoreOwner ViewModelStoreOwner 是在其下具有视图模型的东西。 ViewModelStoreOwner 由活动和片段实现。因此,您有几种创建视图模型的方法:


  1. 活动中的视图模型


  ViewModelProvider(this,YourViewModelFactory)



  1. 片段视图模型


  ViewModelProvider(此,YourViewModelFactory)



  1. 片段(B)中的viewmodel范围为a父片段(A)并在A下的子片段之间共享


  ViewModelProvider(requireParentFragment (),YourViewModelFactory)



  1. viewmodel片段的范围仅限于父级活动,在活动下的各个片段之间共享


  ViewModelProvider(requireActivity(),YourViewModelFactory)



一种解决方法是为共享通用视图模型的片段制作BaseFragment,但这又会包含样板代码,而且我也不是BaseFragment / BaseActivity的忠实粉丝


是的,这确实是个坏主意。解决方案是使用 requireParentFragment() requireActivity()来获取viewmodel实例。但是,您将在具有视图模型的每个片段/活动中编写相同的内容。为避免这种情况,您可以将这个 ViewModelProvider(x,factory)部分抽象化为基片段/活动类,并将工厂注入基类中,这将简化您的孩子片段/活动代码如下:

  class MyFragment:BaseFragment(){

private val vm:ChartViewModel通过bindViewModel()//或bindParentFragmentViewModel()或bindActivityViewModel()


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);
  }
}

解决方案

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()

这篇关于使用dagger2在多个片段中使用视图模型的相同实例的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆