Home » Android » How to solve: "cannot find getter for attribute 'android:text'" when implementing two-way data binding with custom view?

How to solve: "cannot find getter for attribute 'android:text'" when implementing two-way data binding with custom view?

Posted by: admin June 15, 2020 Leave a comment

Questions:

I went through many kinda-similar questions but none of the answers seemed to solve my problem. I implemented a custom EditText that I want to be compatible with two-way data binding. The problem is, every time I try to compile I get the error:

Error:java.lang.IllegalStateException: failed to analyze: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
****/ data binding error ****msg:Cannot find the getter for attribute 'android:text' with value type java.lang.String on com.app.toolkit.presentation.view.CustomEditText. file:/Users/humble-student/Home/workspace/android/application/app/src/main/res/layout/login_view.xml loc:68:8 - 81:69 ****\ data binding error ****

    at org.jetbrains.kotlin.analyzer.AnalysisResult.throwIfError(AnalysisResult.kt:57)
    at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules(KotlinToJVMBytecodeCompiler.kt:137)
    at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:158)
    at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:61)
    at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.java:107)
    at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.java:51)
    at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:92)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1$2.invoke(CompileServiceImpl.kt:386)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$compile$1$2.invoke(CompileServiceImpl.kt:96)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$$inlined$ifAlive$lambda$2.invoke(CompileServiceImpl.kt:892)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl$doCompile$$inlined$ifAlive$lambda$2.invoke(CompileServiceImpl.kt:96)
    at org.jetbrains.kotlin.daemon.common.DummyProfiler.withMeasure(PerfUtils.kt:137)
    at org.jetbrains.kotlin.daemon.CompileServiceImpl.checkedCompile(CompileServiceImpl.kt:919)
    at 

Here is my implementation:

CustomEditText

class CustomEditText @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    // ...
    private lateinit var editText_input: EditText
    private lateinit var textView_errorMessage: TextView

    private var isErrorDisplayed = false
    private var inputTextOriginalColor: ColorStateList? = null

    init {
        orientation = VERTICAL
        clearContainerFormatting()

        createEditTextInput(context, attrs, defStyleAttr)
        createTextViewErrorMessage(context)

        addView(editText_input)
        addView(textView_errorMessage)
    }

    fun setError(message: String) {
        //...
    }

    fun getText(): String = editText_input.text.toString()

    fun setText(text: String) = editText_input.setText(text)

    // ...
}

Model

data class SampleData(
        private var _content: String
) : BaseObservable() {
    var content: String
        @Bindable get() = _content
        set(value) {
            _content = value
            notifyPropertyChanged(BR.content)
        }
}

Client that uses the CustomView with data binding

<?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>

        <import type="android.view.View" />

        <variable
            name="data"
            type="SampleData" />

        <variable
            name="presenter"
            type="SamplePresenter" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:animateLayoutChanges="true"
        tools:context=".sample_view.presentation.view.SampleView">

        <NotificationPopup
            android:id="@+id/notificationPopup"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clipToPadding="false"
            android:elevation="4dp"
            app:allowManualExit="true" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:orientation="vertical">

            <TextView
                android:id="@+id/textView_mirror"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:fontFamily="sans-serif"
                android:text="@{data.content}"
                android:textSize="16sp"
                android:textStyle="bold"
                tools:text="test" />

            <CustomEditText
                android:id="@+id/customEditText_sample"
                style="@style/RegisterInput"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Type anything"
                android:text="@={data.content}" />

            <Button
                android:id="@+id/button_validateInput"
                style="@style/Widget.AppCompat.Button.Colored"
                android:layout_width="150dp"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:onClick='@{(v) -> presenter.onValidateDataClick(customEditTextSample.getText())}'
                android:text="Validate Input" />
        </LinearLayout>
    </RelativeLayout>
</layout>

P.S.: If I replace CustomEditText for regular EditText widget, it works perfectly

How to&Answers:

Funny but I was able to find a great post on medium that helped me with this issue. Basically what I needed was a CustomEditTextBinder:

@InverseBindingMethods(
        InverseBindingMethod(
                type = CustomEditText::class,
                attribute = "android:text",
                method = "getText"
        )
)
class CustomEditTextBinder {
    companion object {
        @JvmStatic
        @BindingAdapter(value = ["android:textAttrChanged"])
        fun setListener(editText: CustomEditText, listener: InverseBindingListener?) {
            if (listener != null) {
                editText.addTextChangedListener(object : TextWatcher {
                    override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {

                    }

                    override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {

                    }

                    override fun afterTextChanged(editable: Editable) {
                        listener.onChange()
                    }
                })
            }
        }

        @JvmStatic
        @BindingAdapter("android:text")
        fun setText(editText: CustomEditText, text: String?) {
            text?.let {
                 if (it != editText.text) {
                     editText.text = it
                 }
            }
         }

It might seem weird but you don’t actually need to call it anywhere, just add the class and the framework will take care of finding it through the annotation processing. Note that the setText is really really important in order to prevent infinite loops. I also added:

var text: String?
    get() = editText_input.text.toString()
    set(value) {
        editText_input.setText(value)
    }

fun addTextChangedListener(listener: TextWatcher) =
        editText_input.addTextChangedListener(listener)

on CustomEditText.

Here is an example of the implementation