DataBinding is not working if setVariable() method is used to set ViewModel

删除回忆录丶 提交于 2019-12-23 05:27:57

问题


I have ParentFragment and ChildFragment. I am using Koin for DI.

In one case, data binding is not working and in another it is working.

NOT WORKING CASE: ParentFragment

abstract class ParentFragment<T: ViewDataBinding, V: ParentViewModel>: Fragment() {

    @LayoutRes
    abstract fun getLayoutResId(): Int

    abstract fun init()

    protected lateinit var binding: T

    protected abstract val mViewModel: V

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)

        return DataBindingUtil.inflate<T>(inflater, getLayoutResId(), container, false).apply { binding = this }.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        doDataBinding()
    }

    private fun doDataBinding() {
        binding.lifecycleOwner = viewLifecycleOwner 
        binding.setVariable(BR.viewModel, mViewModel)
        binding.executePendingBindings()
        init()
    }

ChidlFragment


class ChildFragment: ParentFragment<FragmentChildBinding, ChildViewModel>() {

    @LayoutRes
    override fun getLayoutResId() = R.layout.fragment_child

    override val mViewModel: ChildViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

    }

    override fun init() {
         mViewModel.a()
    }

a() method does nothing except changing the variable value to some random text. This varibale is bound to EditText in ChildFragment. These are basic data binding stuff. Implementation of this method is provided at the end of the question.

The code above working and a() method is being correctly called but EditText value in my ChildFragment is not changing.

WORKING CASE: If I change my code to this, everything is working fine.

ParentFragment

abstract class ParentFragment<T: ViewDataBinding>: Fragment() {

    @LayoutRes
    abstract fun getLayoutResId(): Int

    protected lateinit var binding: T


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)

        return DataBindingUtil.inflate<T>(inflater, getLayoutResId(), container, false).apply { binding = this }.root
    }

ChildFragment

class ChildFragment: ParentFragment<FragmentChildBinding>() {

    @LayoutRes
    override fun getLayoutResId() = R.layout.fragment_child

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val viewModel: ChildViewModel = getViewModel()
        binding.viewModel = viewModel
        binding.lifecycleOwner = this

        binding.viewModel.a()

    }

My ChildViewModel class. Note this class is the same in both of the cases:

class ChildViewModel(): ParentViewModel() {
    var password: String = ""

    //This function is being called in both cases. BUT ONLY IN THE SECOND CASE, setting value
    //to "password" is being shown in the "EditText". 
    fun a () {
        Log.d("-------", "ViewModel method called")
        password = "asdasijdj1n2"
    }
}

What might the problem here?

The reason why I doing this is that I would like to optmize my ParentFragment as much as possible in order to avoid boilerplate code in the children fragments.


回答1:


There are two issues here, but only one is the root cause.

The reason the working case, well, works, is because the value of the password property in the view model is set before databinding actually binds it to the view. The reason this doesn't happen in the not working case is nothing to do with the structure of your fragments - it's simply that binding.executePendingBindings() is called before the value of password is set in the view model. This forces databinding to bind the value of password to view, but as it's null at the time, you don't see anything.

This brings us to the root cause of the issue, which is that you have a property in your view model which is being used by databinding and for which the value changes, but that property is not observable. Databinding needs to know when the value of properties it's using change, so that it can update the views that use those properties. The reason the not working case doesn't work is that databinding was forced to bind the null value for password to the view when binding.executePendingBindings() was called, and had no way of knowing that password was changed later, so it couldn't update the view.

Two ways to make properties observable by databinding are to declare them as LiveData<T> or ObservableField<T> instead of T (where T is the type of data). If password had been declared as a MutableLiveData<String> or an ObservableField<String>, you would have seen the value appear in the view in both cases in your question. This is because databinding would know when the value had changed.

So, in summary, it's good practice to use LiveData or ObservableField for properties declared in view models that are used in databinding and for which the value can change. That way, there's no potential for timing issues with things like when databinding binds values to views.



来源:https://stackoverflow.com/questions/59283991/databinding-is-not-working-if-setvariable-method-is-used-to-set-viewmodel

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!