Reputation: 1857
I have encountered a problem to which I have been so laboring for at least 2 weeks now and I felt so dumbfounded that after so many years I kind of forgot how databinding works and how to correctly set it up for "CUSTOM VIEWS". I decided to check it out on a very simple project to isolate it from my current project. A very simple HelloWorld app which basically outputs Hello World to the screen using Data Binding. The project contains the following files:
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
binding.message = "Hello World!"
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="message" type="String" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.neonapps.android.sample.databinding.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<!-- Please take note I am data binding on my custom view -->
app:message="@{message}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
And now here is the most important part of the problem. This is a custom view CustomView
.I want to bind a particular data 'String' into this view, as such it is able to output "Hello World" on this CustomView:
class CustomView(context : Context, attrs : AttributeSet, defStyleAttrs : Int, defStylRes : Int) : RelativeLayout(context){
constructor(context : Context, attrs : AttributeSet) : this(context, attrs, 0, 0)
constructor(context : Context, attrs : AttributeSet, defStyleAttrs : Int) : this(context, attrs, defStyleAttrs, 0)
private var myMessage : String? = null
set(value){
value.let{
field = it
binding.message = field
}
}
private val binding : LayoutCustomViewBinding = LayoutCustomViewBinding.inflate(LayoutInflater.from(context), this, true)
init {
binding.message?.let{
binding.message = it
}
}
fun setMessage(message : String?){
myMessage = message
}
}
@BindingAdapter(value = ["message"])
fun setMessage(view : TextView, message : String?)
{
message?.let{
view.text = it
}
}
@BindingAdapter(value = ["message"])
fun setMessage(view : CustomView, message : String?)
{
message?.let{
view.message = it
}
}
Here is the catch. This CustomView
inflates a view which can be binded:
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="message" type="String" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:message="@{message}"
tools:text="Hello World"/>
</RelativeLayout>
</layout>
So I basically binding a String onto this custom view (which is composed (supposedly) of many views in its layout) once I set it from outside, like the activity_main.xml
above.
activity_main.kt
<layout
...>
<data>
...
</data>
<android.support.constraint.ConstraintLayout
...>
<com.neonapps.android.sample.databinding.CustomView
...
<!-- Please take note I am data binding on my custom view -->
app:message="@{message}"
.../>
</android.support.constraint.ConstraintLayout>
</layout>
Once I build the entire project, everything seems to work fine. I run now the app and I am getting the following error:
Attempt to invoke virtual method 'void ******.databinding.CustomView.setTag(java.lang.Object)' on a null object reference
at com.neonapps.android.sample.databinding.databinding.ActivityMainBindingImpl.<init>(ActivityMainBindingImpl.java:37)
at com.neonapps.android.sample.databinding.databinding.ActivityMainBindingImpl.<init>(ActivityMainBindingImpl.java:29)
at com.neonapps.android.sample.databinding.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:44)
at android.databinding.MergedDataBinderMapper.getDataBinder(MergedDataBinderMapper.java:74)
at android.databinding.DataBindingUtil.bind(DataBindingUtil.java:199)
at android.databinding.DataBindingUtil.inflate(DataBindingUtil.java:130)
at com.neonapps.android.sample.databinding.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:49)
at com.neonapps.android.sample.databinding.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:43)
at *****.MainActivity.onCreate(MainActivity.kt:12)
at android.app.Activity.performCreate(Activity.java:6904)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1136)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3266)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3415)
at android.app.ActivityThread.access$1100(ActivityThread.java:229)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:7406)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
My app crashes, all of sudden, I went into panic. I just don't know the basics of DataBinding anymore. It works great when I just databinding on views but I am having no luck at all data binding on my own custom views.One thing that is driving me crazy is that this crashes on a code that is autogenerated. I have absolutely no idea how it generated a code referencing on a null it generated without assigning a reference to it. I surrender, there is something I missed badly.
I definitely missed something and I cannot seem to spot it. I kept cross referencing the DataBinding library docs but nothing comes up to me useful.
I tried this code on
Android Studio: 3.4 Canary 7
Kotlin: 1.3.11
Android Studio: 3.2.1
Kotlin: 1.2.71
First I thought it might be Kotlin/Build config/gradle related problem, until I build this project on stable environments and they behave the same regardless.
This is my curse. Any help to lessen my suffering would be appreciated!
Upvotes: 3
Views: 2086
Reputation: 237
Must have all constructors.
Must call the two super constructors.
Like this:
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context,attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
Upvotes: 0
Reputation: 608
This is a bit of a late answer but might help others facing similar issues. I ran into the same issue while developing a customview that was inflating its own layouts for phone/tablet and adding them to the view hierarchy. For me, turns out the issue was using the generated Binding object to inflate the layout inside this custom view. It all had to do with a function of ViewDataBinding.java
which recursively goes through the view hierarchy and gets the binding objects for each child.
private static void mapBindings(DataBindingComponent bindingComponent, View view,
Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
boolean isRoot) {
final int indexInIncludes;
final ViewDataBinding existingBinding = getBinding(view);
if (existingBinding != null) {
return;
}
...
}
The return statement was causing the mapping to exit out from the recursive function too early and my main binding class's views were null hence the crash.
Here's my custom view for reference:
class CustomContainerView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs,
defStyleAttr)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int,
defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
var binding: CustomContainerViewBinding? = null
val customViewRoot: View
init {
orientation = VERTICAL
customViewRoot= LayoutInflater.from(context)
.inflate(R.layout.custom_container_view, this, false)
addView(customViewRoot)
if (!isInEditMode) {
binding = CustomContainerViewBinding.bind(customViewRoot)
}
}
private fun relocate(child: View) {
removeView(child)
val params = child.layoutParams ?: ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
findViewById<ViewGroup>(R.id.contentContainer)
.addView(child, params)
}
override fun onViewAdded(child: View?) {
child?.let {
if (it != customViewRoot) {
relocate(it)
}
}
}
}
So for me the culprit was this line:
if (!isInEditMode) {
binding = CustomContainerViewBinding.bind(customViewRoot)
}
To fix it, I just remove the above code from the init block and moved it to a lazy load getter.
class CustomContainerView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs,
defStyleAttr)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int,
defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
private var customContainerBinding: CustomContainerViewBinding? = null
val customViewRoot: View
init {
orientation = VERTICAL
customViewRoot= LayoutInflater.from(context)
.inflate(R.layout.custom_container_view, this, false)
addView(customViewRoot)
}
private fun relocate(child: View) {
removeView(child)
val params = child.layoutParams ?: ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
findViewById<ViewGroup>(R.id.contentContainer)
.addView(child, params)
}
override fun onViewAdded(child: View?) {
child?.let {
if (it != customViewRoot) {
relocate(it)
}
}
}
fun getCustomContainerBinding(): CustomContainerViewBinding? {
if (customContainerBinding == null) {
customContainerBinding = CustomContainerViewBinding.bind(customViewRoot)
}
return customContainerBinding
}
}
The same thing could be happening for OP's question because it looks like you're relying on your Binding class to inflate your layout.
private val binding : LayoutCustomViewBinding = LayoutCustomViewBinding.inflate(LayoutInflater.from(context), this, true)
So to fix it, just follow the regular approach of inflating LayoutInflater.inflate()
to create your view, then create a getter function that can lazy bind to the Binding object.
class CustomView(context : Context, attrs : AttributeSet, defStyleAttrs : Int, defStylRes : Int) : RelativeLayout(context){
constructor(context : Context, attrs : AttributeSet) : this(context, attrs, 0, 0)
constructor(context : Context, attrs : AttributeSet, defStyleAttrs : Int) : this(context, attrs, defStyleAttrs, 0)
private var myMessage : String? = null
set(value){
value.let{
field = it
binding?.message = field
}
}
private val rootView : View
private val binding : LayoutCustomViewBinding?
get() = LayoutCustomViewBinding.bind(rootView)
init {
rootView = LayoutInflater.from(context)
.inflate(R.layout.layout_custom_view, this, true)
}
fun setMessage(message : String?){
myMessage = message
}
}
@BindingAdapter(value = ["message"])
fun setMessage(view : TextView, message : String?)
{
message?.let{
view.text = it
}
}
@BindingAdapter(value = ["message"])
fun setMessage(view : CustomView, message : String?)
{
message?.let{
view.message = it
}
}
Upvotes: 1
Reputation:
This is my curse. Any help to lessen my suffering would be appreciated!
I'll try.
Remove the two BindingAdapters you wrote and rewrite your class to this:
class CustomView : RelativeLayout {
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0, 0)
constructor(context: Context, attrs: AttributeSet, defStyleAttrs: Int) :
this(context, attrs, defStyleAttrs, 0)
constructor(context: Context, attrs: AttributeSet, defStyleAttrs: Int, defStylRes: Int) :
super(context, attrs, defStyleAttrs, defStylRes)
private val binding: LayoutCustomViewBinding = LayoutCustomViewBinding.inflate(LayoutInflater.from(context), this, true)
var message: String? = null
set(value) {
binding.message = value
}
}
Replace the app:message="@{message}"
part in the layout of your CustomView with android:text="@{message}"
:
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="message" type="String" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{message}"
tools:text="Hello World"/>
</RelativeLayout>
</layout>
Explanation:
You will not need the BindingAdapters that you wrote as the databinding library automatically detects the setMessage() method that is generated from the message field in your Kotlin class during compilation. The same holds for the TextView, the databinding library will detect that there is a setText() method and use it for android:text="@{message}"
.
Upvotes: 1