问题
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