How to avoid Android DataBinding NPE related crashes
During investigating and fixing a crash in one of the apps I’m contributing, I found an issue with DataBinding and custom adapters I’d like to share with you.
Initial state
Sample ViewModel with the data class I prepared to contain the info for the binding:
class ViewModel() {
data class Parent(val parentId: String, val child: Child?) {
data class Child(val childId: String)
}
val sampleData = Parent("parentId", null)
@get:Bindable
var parent: Parent = sampleData
set(value) {
.
.
.
}
}
Initial adapter:
@BindingAdapter("initialBinding")
fun setInitialBinding(view: View, child: ViewModel.Parent.Child) {
...
}
Wire up adapter and ViewModel in the xml layout:
<View
initialBinding="@{vm.parent.child}">
What we got runtime:
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter child
Fix
For a quick fix, after checking another similar adapter I adjusted the code with a ?
in the adapter parameters like this:
@BindingAdapter("fixedBinding")
fun setFixedBinding(view: View, child: ViewModel.Prent.Child?) {
...
}
Everything went well, after the release we never saw again this kind of crash. I closed the issue in Crashlytics after monitoring it for a couple of days.
Further improvements
However, my brain could not stop, and I was thinking further about:
- How to avoid this kind of issue in the future?
- How can I create a more robust solution?
- How to convert this runtime exception to something that can be found compile time?
During this journey I found the real issue we have. We’re not enforced in xml to feed our initial custom binding adapter with proper value, which is non null in our case. So the compiler is not asking for something like this in the xml:
<View
idealBinding="@{ vm.parent?.child ?: Parent("parentId", Child("childId")) }">
Finally, I understood, that this is the real problem: we have proper information about our data both in ViewModel and in the adapter, but the middleware (the xml) is not clever enough for us.
Long term solution
To answer both 3 questions I had above I’d suggest the following: remove as much logic from xmls as possible, and pass to the custom adapters the bindable data completely like this:
<View
goodBinding="@{vm.parent}">
And use a binding adapter like this:
@BindingAdapter("goodBinding")
fun setGoodBinding(view: View, parent: ViewModel.Parent) {
val child: ViewModel.Parent.Child? = parent?.child
...
}
With this approach, you moved the logic into the adapter, which is a Kotlin file, where you can gain the advantage of the compiler’s feature, that it will warn you: getting the child
can give you a null result, which should be handled.
I hope I could help you!
Máté