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:
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)
// ...
}
data class SampleData(
private var _content: String
) : BaseObservable() {
var content: String
@Bindable get() = _content
set(value) {
_content = value
notifyPropertyChanged(BR.content)
}
}
<?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
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 .
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.
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.
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
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