Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 400
Humble Student Avatar asked Dec 16 '17 20:12

Humble Student


People also ask

How check data binding is null in Android?

Generated data binding code automatically checks for null values and avoid null pointer exceptions. For example, in the expression @{user.name} , if user is null, user.name is assigned its default value of null . If you reference user. age , where age is of type int , then data binding uses the default value of 0 .

What is two-way DataBinding Android?

Two-way Data Binding is a technique of binding your objects to your XML layouts so that the layout can send data to your binding object. This is compared to a “traditional” or “one-way” Data Binding setup, where data would only move from your binding object to the layout.

What is one way binding and two-way binding in Android?

Data binding in AndroidUpdating the views from the data source is a simple one-way binding. In that case, you'll only access data from the data source and update the layout. Two-way data binding is nothing but updating the data source if there are any changes in the layout and vice versa.


1 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

like image 122
Humble Student Avatar answered Oct 05 '22 07:10

Humble Student