I am trying to create a simple form layout that uses a ViewModel containing LiveData to bind to each form field so that the inputted data can persist through configuration changes and back navigation (in the case of a multi-step form). Additionally, I have a custom form field view with its own layout xml to do some required processing. However, I am having difficulty passing data through the form layout to bind to the custom view layout.
Here is a simplified version of my ViewModel, Fragment, Field object and its layout:
class FormViewModel : ViewModel() {
@Bindable val email = MutableLiveData<String>().apply { value = "" }
}
class FormFragment : BaseFragment() {
private val formViewModel: FormViewModel by lazy {
ViewModelProviders.of(requireActivity()).get(FormViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?) : View? =
(DataBindingUtil
.inflate(inflater, R.layout.fragment_form, container, false) as FragmentFormBinding)
.apply {
lifecycleOwner = requireActivity()
viewModel = formViewModel
}
.root
}
class Field @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr) {
@Bindable var fieldData: MutableLiveData<String> = MutableLiveData<String>().apply { value = "" }
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
(DataBindingUtil
.inflate(inflater, R.layout.field, this, true) as FieldBinding)
.apply { fieldData = [email protected] }
.root
}
}
field.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="fieldData"
type="androidx.lifecycle.MutableLiveData<String>"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/inputField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:hint="Email">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Email"
android:text="@={ fieldData }" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>
What I would like to do is bind the live data from the ViewModel to a form field by passing it as an attribute to an instance of my custom form field in the form xml like so:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewModel"
type="packageName.FormViewModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="32dp">
<packageName.Field
android:id="@+id/emailField"
app:fieldData="@{ viewModel.email }"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="60dp" />
</LinearLayout>
</layout>
However, when I try this I get the following error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: packageName, PID: 27543
java.lang.RuntimeException: Failed to call observer method
at androidx.lifecycle.ClassesInfoCache$MethodReference.invokeCallback(ClassesInfoCache.java:226)
at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeMethodsForEvent(ClassesInfoCache.java:194)
at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeCallbacks(ClassesInfoCache.java:185)
at androidx.lifecycle.ReflectiveGenericLifecycleObserver.onStateChanged(ReflectiveGenericLifecycleObserver.java:37)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:361)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:188)
at androidx.databinding.ViewDataBinding.setLifecycleOwner(ViewDataBinding.java:394)
at packageName.FormFragment.onCreateView(FormFragment.kt:24)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2669)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1321)
at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2515)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2290)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2246)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2143)
at androidx.fragment.app.FragmentManager$3.run(FragmentManager.java:417)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter <set-?>
at packageName.Field.setFieldData(Field.kt)
at packageName.databinding.FragmentFormBindingImpl.executeBindings(FragmentFormBindingImpl.java:127)
at androidx.databinding.ViewDataBinding.executeBindingsInternal(ViewDataBinding.java:472)
at androidx.databinding.ViewDataBinding.executePendingBindings(ViewDataBinding.java:444)
at androidx.databinding.ViewDataBinding$OnStartListener.onStart(ViewDataBinding.java:1685)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at androidx.lifecycle.ClassesInfoCache$MethodReference.invokeCallback(ClassesInfoCache.java:216)
at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeMethodsForEvent(ClassesInfoCache.java:194)
at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeCallbacks(ClassesInfoCache.java:185)
at androidx.lifecycle.ReflectiveGenericLifecycleObserver.onStateChanged(ReflectiveGenericLifecycleObserver.java:37)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:361)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:188)
at androidx.databinding.ViewDataBinding.setLifecycleOwner(ViewDataBinding.java:394)
at packageName.FormFragment.onCreateView(FormFragment.kt:24)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2669)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1321)
at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2515)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2290)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2246)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2143)
at androidx.fragment.app.FragmentManager$3.run(FragmentManager.java:417)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
As an alternative this can easily be achieved by including the field layout instead like the following but in this case I cannot do any of the setup I would like to do in the Field object
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewModel"
type="packageName.FormViewModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="32dp">
<include
layout="@layout/field"
android:id="@+id/emailField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:fieldData="@{ viewModel.email }" />
</LinearLayout>
</layout>
I'm guessing has to do with the order in which the xml is inflated and data binding variable generated but I am having difficulty debugging
EDIT: I just noticed I am not setting the lifecycle owner in the Field data binding but the issue arises even if I am not inflating the layout in the Field class
This example shows how to bind to XML data using an XmlDataProvider. With an XmlDataProvider, the underlying data that can be accessed through data binding in your application can be any tree of XML nodes. In other words, an XmlDataProviderprovides a convenient way to use any tree of XML nodes as a binding source.
The root node of the XML data has an xmlnsattribute that sets the XML namespace to an empty string. This is a requirement for applying XPath queries to a data island that is inline within the XAML page.
With an XmlDataProvider, the underlying data that can be accessed through data binding in your application can be any tree of XML nodes. In other words, an XmlDataProviderprovides a convenient way to use any tree of XML nodes as a binding source.
But if you want to use your custom view with data binding component actually you don’t need to handle this attributes. data binding library has automatic method selection ability, that means for an attribute named currencyCode, the library automatically tries to find the method setCurrencyCode (arg) that accepts compatible types as the argument.
Try using @BindingAdapter("...") instead of the @Bindable annotation.
I made a sample repo that reflects and solves your problem, maybe have a look.
https://github.com/phamtdat/BindingAdapterSample
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With