使用dagger2在多个片段中使用视图模型的相同实例

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

片段和视图模型代码如下:

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...
}

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

//-----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
}

有什么方法可以为多个片段实现相同的视图模型实例,也可以同时将视图模型注入片段中。 另外,也需要bindViewModelFactory方法,因为即使没有此方法,它似乎也对应用程序没有影响。

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

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

@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);
  }
}
评论
  • nodio
    nodio 回复

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

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

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

    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.

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

    否。由于上述原因。

    另外,也需要bindViewModelFactory方法,因为即使没有此方法,它似乎也对应用程序没有影响。

    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

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

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

    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 keeps track of the viewmodels under them. ViewModelStoreOwner is implemented by activity and fragment. So you have a few ways of creating a viewmodel:

    1. 活动中的视图模型
    ViewModelProvider(this, YourViewModelFactory)
    
    1. 片段中的视图模型
    ViewModelProvider(this, YourViewModelFactory)
    
    1. 片段中的viewmodel范围为父片段,并在子片段之间共享
    ViewModelProvider(requireParentFragment(), YourViewModelFactory)
    
    1. 片段中的viewmodel范围限定于父活动,并在活动下的各个片段之间共享
    ViewModelProvider(requireActivity(), YourViewModelFactory)
    
    一种解决方法是为共享通用视图模型的片段制作BaseFragment,但是它将再次包含样板代码,而且我也不是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 code multiple times in every fragment/activity that has a viewmodel. For 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: Fragment() {
    
        private val vm: ChartViewModel by bindViewModel() // or bindParentFragmentViewModel() or bindActivityViewModel()
    
  • {醉相思}
    {醉相思} 回复

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

    片段一

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

    片段二

    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")
    
     }
    }
    
  • 换位思考
    换位思考 回复

    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 theViewModelProviders.of() method as the factory (second parameter)

    class PostListActivity : AppCompatActivity() {
    
        @Inject
        lateinit var viewModelFactory: ViewModelProvider.Factory
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_post_list)
            getAppInjector().inject(this)
            val vm = ViewModelProviders.of(this, viewModelFactory)[PostListViewModel::class.java]
            vm.posts.observe(this, Observer(::updatePosts))
        }
    
        //...
    }
    
    

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