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

dee*_*mar 5 android viewmodel android-fragments dagger-2 android-mvvm

我在我的项目中只使用 dagger2 (不是 dagger-android)。使用多重绑定注入 ViewModel 工作正常。但是以前没有 dagger2 有一个问题,我在多个片段的活动中使用了相同的视图模型实例(使用 fragment-ktx 方法 activityViewModels()),但现在由于dagger2 正在注入视图模型,它总是提供新实例(在每个片段的视图模型的每个片段中使用 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...
}
Run Code Online (Sandbox Code Playgroud)

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

//-----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
}
Run Code Online (Sandbox Code Playgroud)

有什么方法可以为多个片段实现相同的视图模型实例,并同时在片段中注入视图模型。 是否还需要 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);
  }
}
Run Code Online (Sandbox Code Playgroud)

son*_*net 5

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

class MyFragment: Fragment() {
    @Inject lateinit var chartViewModel: ChartViewModel
Run Code Online (Sandbox Code Playgroud)

dagger 只是创建一个新的 viewmodel 实例。没有 viewmodel-fragment-lifecycle 魔法正在进行,因为这个 viewmodel 不在活动/片段的 viewmodelstore 中,并且不是由您创建的 viewmodelfactory 提供的。在这里,您可以真正将视图模型视为任何普通类。举个例子:

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

您的 viewmodel 与此等效,AnyClass因为 viewmodel 不在 viewmodelstore 中,也不限于片段/活动的生命周期。

有什么方法可以为多个片段实现相同的视图模型实例,并同时在片段中注入视图模型

不会。因为上面列出的原因。

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

它没有任何影响,因为(我假设)您没有在ViewModelFactory任何地方使用。由于它没有在任何地方被引用,viewmodelfactory 的这个匕首代码是没有用的。

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

Run Code Online (Sandbox Code Playgroud)

这是@binds 正在做的事情:1 2

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

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

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

    private val vm: ChartViewModel by lazy {
        ViewModelProvider(X, YourViewModelFactory).get(ChartViewModel::class.java)
    }
Run Code Online (Sandbox Code Playgroud)

什么是X在这里吗?X 是ViewModelStoreOwner。AViewModelStoreOwner是在它们下面有视图模型的东西。ViewModelStoreOwner由活动和片段实现。所以你有几种创建视图模型的方法:

  1. 活动中的视图模型
ViewModelProvider(this, YourViewModelFactory)
Run Code Online (Sandbox Code Playgroud)
  1. 片段中的视图模型
ViewModelProvider(this, YourViewModelFactory)
Run Code Online (Sandbox Code Playgroud)
  1. 片段 (B) 中的视图模型范围为父片段 (A) 并在 A 下的子片段之间共享
ViewModelProvider(requireParentFragment(), YourViewModelFactory)
Run Code Online (Sandbox Code Playgroud)
  1. 片段中的视图模型范围为父活动并在活动下的片段之间共享
ViewModelProvider(requireActivity(), YourViewModelFactory)
Run Code Online (Sandbox Code Playgroud)

一种解决方法可能是为共享公共视图模型的片段创建一个 BaseFragment,但这将再次包含样板代码,而且我也不是 BaseFragment/BaseActivity 的忠实粉丝

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

class MyFragment: BaseFragment() {

    private val vm: ChartViewModel by bindViewModel() // or bindParentFragmentViewModel() or bindActivityViewModel()
Run Code Online (Sandbox Code Playgroud)

  • @AndroidDev123所以假设你有一个父片段A,在A下你有另外三个子片段B、C和D。如果你在B、C和D中执行`requireParentFragment()`,则意味着视图模型的范围为Fragment A 的生命周期是 B、C 和 D 的父级。只有当父级 Fragment A 被销毁时,视图模型才会被销毁。 (2认同)