我的ViewModel无法承受配置更改

时间:2018-11-17 21:41:50

标签: java android mvvm kotlin dagger-2

我正在使用MVVM尝试Dagger 2。这是我到目前为止所拥有的,

我的ViewModel绑定模块:

@Module
public abstract class ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(MedicationsViewModel.class)
    @Singleton
    abstract ViewModel bindMedicationsViewModel(MedicationsViewModel viewModel);

    @Binds
    @Singleton
    abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory factory);
}

我的片段:

class MedicationsFragment : DaggerFragment() {
    private lateinit var binding : FragmentMedicationsBinding
    private lateinit var viewModel: MedicationsViewModel

    @Inject lateinit var viewModelFactory : ViewModelFactory

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        binding = FragmentMedicationsBinding.inflate(inflater, container, false)

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(MedicationsViewModel::class.java)

        val adapter = MedicationAdapter()
        binding.rvMedications.adapter = adapter

        viewModel.getMedications().observe(viewLifecycleOwner, Observer { items ->
            if (items != null) {adapter.submitList(items); adapter.notifyDataSetChanged()}
        })

        return binding.root
    }
}

我的ViewModel是这样的:

class MedicationsViewModel @Inject constructor(
    private val repository: MedicationRepository,
    ): ViewModel() {
        private var medications : MutableLiveData<MutableList<Medication>> = MutableLiveData()
        private val disposable: CompositeDisposable = CompositeDisposable()

        fun getMedications() : MutableLiveData<MutableList<Medication>>{
            getMockMedications()
            return medications }

        private fun getMockMedications(){
            val medication1 = Medication(1,"Mock Med 1", "2018-01-01","once per day",true,null)
            val medication2 = Medication(2,"Mock Med 2", "2018-01-02","once per day",false,"before 15 minutes")

            val mockList: MutableList<Medication> = mutableListOf()
            mockList.add(medication1)
            mockList.add(medication2)

            medications.postValue(mockList)
        }

        fun addMedication(medication: Medication){
            medications.value?.add(medication)
            medications.notifyObserver()
        }

        fun <T> MutableLiveData<T>.notifyObserver() {
            this.postValue(this.value)
        }
    }

通过片段中的一个按钮,我正在打开一个活动,并将另一个Medication项目添加到我的视图模型中。从活动结束()后,我可以在RecyclerView中看到片段中新添加的药物项目。但是,当我旋转屏幕时,我丢失了新添加的项目,并再次离开了我的模拟药物项目。这是我认为正在创建我的视图模型的新实例的地方。

问题可能是Dagger每当重新创建我的片段(和容器活动)时都创建我的视图模型的新实例的原因。但是我认为使用@Singleton范围注释ViewModelFactory可以解决此问题。

谁能看到我所缺少的东西?

谢谢。

2 个答案:

答案 0 :(得分:2)

您在这里混合了至少4种不同的东西。


  

问题可能是由于Dagger每当重新创建我的片段(和容器活动)时都为视图模型创建新实例的原因。

在注入片段时,您声明viewModel将被字段注入(因为在属性上添加了@Inject注释...)

@Inject lateinit var viewModelFactory : ViewModelFactory

这意味着Dagger将在您调用AndroidInjection.inject(..)时注入字段。由于您使用构造函数注入(在@Inject的构造函数上使用MedicationsViewModel),Dagger每次都会创建一个 new 对象(您没有限制类的作用),但是您会绝对不要使用该对象,因为...


发生后,您立即使用ViewModelFactory

中的viewModel覆盖了该字段
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MedicationsViewModel::class.java)

由于ViewModelFactory使用@Binds @IntoMap @Singleton bindMedicationsViewModel,因此应该重用相同的对象,并且受@Singleton限制。您没有共享实现,所以如果它不是同一对象,则可能无法正确处理@Singleton组件。

更多问题...

  

但是我认为使用@Singleton范围注释ViewModelFactory可以解决此问题。

通常,您应该将作用域放在类上而不是绑定上。

@Singleton // scope the class!
class MedicationsViewModel @Inject constructor()

这是因为通常是一个实现细节,它包含了什么范围。但是无论如何,您完全不应该在这里使用@Singleton,因为您试图使用ViewModelthing 。如果要将@Singleton用于ViewModel,则不需要整个ViewModelProviders。如您所见,这只会导致错误和混乱,因为两者的作用域不同且处理方式不同。

更好的设置

假设您要使用ViewModel,请用Dagger限制ViewModel的范围。您让Android Arch组件处理事情。您仅使用Dagger删除对象创建的样板,以便ViewModel.Factory可以在需要时轻松创建新的ViewModel 。您可以通过将ViewModel绑定到传递给工厂的集合(带有<Class, Provider<ViewModel>>的集合)中来进行操作,因此,调用provider.get()将总是按预期方式创建一个新对象。生命周期毕竟是由arch组件处理的。

@Binds
@IntoMap
@ViewModelKey(MedicationsViewModel.class) // no scope here!
abstract ViewModel bindMedicationsViewModel(MedicationsViewModel viewModel);

// neither here!
class MedicationsViewModel @Inject constructor()

设置正确后,ViewModelStoreOwner(ViewModelProviders.of(this))将管理ViewModels生命周期,并决定何时重新使用或重新创建模型。如果要在ViewModel上引入Dagger范围,则它要么什么都不做(如果范围较短,则可能会导致错误或意外行为(使用@Singleton或寿命较长的范围时)

另外,ViewModel.Factory也应该不受作用域

@Binds // no scope here either!
abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory factory);

这样一来,您还可以在Fragment / Activity范围内的组件上添加ViewModels,并使您能够注入非单例范围内的类。这将大大提高灵活性。该类的作用域也没有任何好处,因为ViewModels不会在配置更改期间存储在其中,并且不保留任何状态。

最后但并非最不重要的一点是,您没有使用Dagger注入字段,而是使用ViewModelProviders对其进行了初始化。尽管它不应该做任何事情,但由于稍后会再次覆盖它,因此它仍然是难闻的气味,只会导致混乱。

// in your fragment / acitvity
lateinit var viewModelFactory : ViewModelFactory // no @Inject for the viewmodel here!

// it will be assigned in onCreate!
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MedicationsViewModel::class.java)

我希望这可以解决一些问题!

答案 1 :(得分:0)

如果这对其他人有帮助,则可能需要将viewModel实例移动到onActivityCreated,以确保已创建viewModelStore。

@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState)
    {
        super.onActivityCreated(savedInstanceState);
        viewModel = ViewModelProviders.of(this, viewModelProviderFactory).get(MedicationsViewModel.class);
    ... 
    }