Custom View with Two-Way Databinding - android

I have a custom view that extends LinearLayout and inflates a Layout which contains a couple of Views.
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
xmlns:android="http://schemas.android.com/apk/res/android">
<EditText
android:id="#+id/voice_edittext"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:hint="Add answer here"
/>
<ImageButton
android:id="#+id/microphone_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_mic_black_24dp"
/>
<ImageButton
android:id="#+id/delete_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_cancel_black_24dp"
android:visibility="gone"
/>
</LinearLayout>
I now want to two-way bind the text value of the Edittext when I use this view, like this:
<com.sunilson.quizcreator.presentation.views.EditTextWithVoiceInput
android:id="#+id/form_question"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
app:editTextValue="#={viewModel.observableText}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
For this I created some Binding Adapters
#BindingAdapter("editTextValueAttrChanged")
fun setListener(editTextWithVoiceInput: EditTextWithVoiceInput, listener: InverseBindingListener) {
editTextWithVoiceInput.voice_edittext.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(p0: Editable?) {
listener.onChange()
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
})
}
#BindingAdapter("editTextValue")
fun setTextValue(editTextWithVoiceInput: EditTextWithVoiceInput, value: String?) {
if (value != editTextWithVoiceInput.voice_edittext.text.toString()) editTextWithVoiceInput.voice_edittext.setText(value)
}
#InverseBindingAdapter(attribute = "editTextValue")
fun getTextValue(editTextWithVoiceInput: EditTextWithVoiceInput): String? {
return editTextWithVoiceInput.voice_edittext.text.toString()
}
This is the code of the view:
class EditTextWithVoiceInput(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) {
init {
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.voice_edittext, this, true)
view.microphone_button.setOnTouchListener { p0, p1 ->
...
}
}
}
The problem now is that when the Fragment containing the view is started, I get this error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.sunilson.quizcreator, PID: 12627
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.sunilson.quizcreator.presentation.views.EditTextWithVoiceInput.setTag(java.lang.Object)' on a null object reference
at com.sunilson.quizcreator.databinding.FragmentAddQuestionBinding.<init>(FragmentAddQuestionBinding.java:112)
at android.databinding.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:15)
at android.databinding.DataBindingUtil.bind(DataBindingUtil.java:199)
at android.databinding.DataBindingUtil.inflate(DataBindingUtil.java:130)
at android.databinding.DataBindingUtil.inflate(DataBindingUtil.java:95)
at com.sunilson.quizcreator.presentation.SingleActivity.fragments.AddQuestionFragment.AddQuestionFragment.onCreateView(AddQuestionFragment.kt:34)
at android.support.v4.app.Fragment.performCreateView(Fragment.java:2425)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1460)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:802)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2623)
at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2410)
at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2365)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2272)
at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:733)
at android.os.Handler.handleCallback(Handler.java:789)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:180)
at android.app.ActivityThread.main(ActivityThread.java:6944)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:835)
What am I missing here?

Ok, so my problem was that my custom view had incorrectly implemented the constructor functions. As I inflate this view in XML, I needed the constructor where I get passed the Attributeset to pass that Attributeset to the super constructor, which I did not do. Without that, my view had no attributes and could not be found via it's ID etc.
Now I have two constructors, depending on if I inflate from XML or code:
constructor(context: Context, optional: Boolean) : super(context) {
...
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
...
}

Related

Databinding in custom ViewGroup give error

TLDR;
I receive an error says :
Unable to resume activity {...MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.appcompat.widget.AppCompatTextView.setTag(java.lang.Object)' on a null object reference
How can I use databinding in Custom ViewGroup?
I created a custom ViewGroup with its layout like :
<?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">
<androidx.cardview.widget.CardView
android:id="#+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="#dimen/margin_8dp"
android:minHeight="72dp"
android:orientation="vertical"
app:cardBackgroundColor="#color/white"
app:cardCornerRadius="#dimen/margin_8dp"
app:cardElevation="#dimen/margin_2dp"
app:strokeColor="#color/color_divider"
app:strokeWidth=".5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/clRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/margin_16dp">
<Some Views Here
...
...
/>
<LinearLayout
android:id="#+id/llContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/margin_16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tvHeader"
tools:background="#color/all_stock_back"
tools:layout_height="200dp" />
<Some Views Here
...
...
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
And View code like:
class CustomLayout : LinearLayout {
private lateinit var mBinding: CustomLayoutBinding
private lateinit var containerLayout: LinearLayout
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initArgs(context, attrs)
initView(context)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
initArgs(context, attrs)
initView(context)
}
private fun initView(context: Context) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
mBinding =
DataBindingUtil.inflate(inflater, R.layout.custom_layout, this, true)
containerLayout = mBinding.llContainer
}
private fun initArgs(context: Context, attrs: AttributeSet?) {
//set attrs
}
override fun onFinishInflate() {
super.onFinishInflate()
mBinding.run {
//Set texts and listeners
}
}
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
val id = child?.id
if (id == # IDsOnLayout
) {
super.addView(child, index, params);
} else {
containerLayout.addView(child, index, params);
}
}
}
When I use this view in any activity/fragment and bind some text or anything to any of its child views like (for simplicity I removed attributes here):
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="vm"
type="SomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<CustomLayout
android:id="#+id/customid"
app:collapsable="false">
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2" android:rowCount="4">
<androidx.appcompat.widget.AppCompatTextView
android:text="#{vm.headerText}" />
<androidx.appcompat.widget.AppCompatTextView />
</GridLayout>
</CustomLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I receive an error says :
Unable to resume activity {...MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.appcompat.widget.AppCompatTextView.setTag(java.lang.Object)' on a null object reference
In generated DBImpl textviews inside my customlayout are null. I guess there is some problem with addview and DBImpl generation.
How can I use databinding in Custom ViewGroup?

Android custom edit text value is changed by another custom edit text

Intro
In one of my project I tried to create custom EditText with header and some custom validations. I came into a strange problem when I tested this custom view with screen rotation and activity recreation.
What is problem
Before recreation
When app starts all edit text have correct values which were set statically from activity. As on picture bellow:
After recreation
After I rotate screen or recreate activity EditText's values will be messed up. CustomEditText values are set to value of last edit text in XML. Simple (Basic Android EditText) edit text values are set normally.
Codes
I copied codes from project where this problem occurs.
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
first_custom_edit_text.header = "First header"
first_custom_edit_text.setText("First text")
third_custom_edit_text.header = "Third header"
third_custom_edit_text.setText("Third text")
first_simple_edit_text.setText("First simple - Not affected")
second_custom_edit_text.header = "Second header"
second_custom_edit_text.setText("Second text")
second_simple_edit_text.setText("Second simple - Not affected")
}
}
CustomEditText
class CustomEditText : LinearLayout {
fun setText(value: String?){
this.input_edit_text.text = Editable.Factory.getInstance().newEditable(value ?: "")
}
fun getText(): String {
return this.input_edit_text.text.toString()
}
var header: String?
get() = this.header_text_view.text.toString()
set(value) {
this.header_text_view.text = Editable.Factory.getInstance().newEditable(value ?: "")
}
constructor(context: Context) : super(context){
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs){
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
inflate(context, R.layout.ui_custom_edit_text, this)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<com.example.customedittextbug.CustomEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/first_custom_edit_text"/>
<com.example.customedittextbug.CustomEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/second_custom_edit_text"/>
<EditText
tools:hint="input#hint.example"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:textColor="#android:color/black"
android:textSize="18sp"
android:inputType="text"
android:id="#+id/first_simple_edit_text"/>
<com.example.customedittextbug.CustomEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/third_custom_edit_text"/>
<EditText
tools:hint="input#hint.example"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:textColor="#android:color/black"
android:textSize="18sp"
android:inputType="text"
android:id="#+id/second_simple_edit_text"/>
</LinearLayout>
ui_custom_edit_text.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
tools:text="Input header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#android:color/black"
android:textStyle="bold"
android:textSize="17sp"
android:id="#+id/header_text_view"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="#+id/validations_errors_holder"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/common_input_holder">
<EditText
tools:hint="input#hint.example"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:textColor="#android:color/black"
android:textSize="18sp"
android:inputType="text"
android:id="#+id/input_edit_text"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="#+id/input_edit_text"
android:layout_centerVertical="true"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
android:gravity="end"
android:orientation="horizontal"
android:id="#+id/right_view_holder"/>
</RelativeLayout>
</LinearLayout>
UPDATE
I found those two guides with nice explanation how to fix this problem after my question was answered.
Link1, Link2
State restoration is keyed by ID, and all of your custom views have a sub-View with the same ID: input_edit_text. Thus, they all get restored to the same state because they all got the last one that was saved under that ID.
You could avoid this by setting android:saveEnabled="false" on that EditText (though you'll probably want to do the save/restore of instance state yourself in your CustomEditText).
I was tired of searching but this worked for me.
add to CustomEditText class
companion object {
private const val SPARSE_STATE_KEY = "SPARSE_STATE_KEY"
private const val SUPER_STATE_KEY = "SUPER_STATE_KEY"
}
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
override fun onSaveInstanceState(): Parcelable? {
Log.i("ByHand", "onSaveInstanceState")
return Bundle().apply {
Log.i("ByHand", "Writing children state to sparse array")
putParcelable(SUPER_STATE_KEY, super.onSaveInstanceState())
putSparseParcelableArray(SPARSE_STATE_KEY, saveChildViewStates())
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
Log.i("ByHand", "onRestoreInstanceState")
var newState = state
if (newState is Bundle) {
Log.i("ByHand", "Reading children children state from sparse array")
val childrenState = newState.getSparseParcelableArray<Parcelable>(SPARSE_STATE_KEY)
childrenState?.let { restoreChildViewStates(it) }
newState = newState.getParcelable(SUPER_STATE_KEY)
}
super.onRestoreInstanceState(newState)
}
fun ViewGroup.saveChildViewStates(): SparseArray<Parcelable> {
val childViewStates = SparseArray<Parcelable>()
children.forEach { child -> child.saveHierarchyState(childViewStates) }
return childViewStates
}
fun ViewGroup.restoreChildViewStates(childViewStates: SparseArray<Parcelable>) {
children.forEach { child -> child.restoreHierarchyState(childViewStates) }
}
this link for details

Sibling view prevents recyclerview from scrolling when made clickable

My screen consists of a ConstraintLayout with two children. The one child is a RecyclerView which takes up the entire screen. The other child is another ConstraintLayout that contains a TextView (the screen is actually more complex than this, but I'm reducing it to something here to make it easier to comprehend). The child ConstraintLayout is positioned over the RecyclerView and scrolls up whenever you scroll the RecyclerView. This is done using a scroll listener on the RecyclerView and re-adjusting the Y position of the child ConstraintLayout. The child ConstraintLayout needs to be clickable.
The problem I am having is that if you try scrolling the RecyclerView by first touching down on the child ConstraintLayout, the scrolling will not work. The click event handler for the child ConstraintLayout will work, however.
From my understanding of how Android handles touch events, if a view returns false in its onTouchEvent handler, that view will not get notified of any further motion events. So theoretically, if my child ConstraintLayout returns false in its onTouchEvent handler, the RecyclerView should still get the scroll motion events.
Here is my layout xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:background="#55338855">
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="#+id/dashboardConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:layoutDescription="#xml/dashboard_initial_animation_start"
app:showPaths="false">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/contentRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipChildren="false"
android:fitsSystemWindows="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/guideWelcome"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="1.0" />
<ReservationCard
android:id="#+id/reservationConstraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="#color/dashboard_reservation_card_background_color"
android:padding="16dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/txtSearch">
<TextView
android:id="#+id/txtReservationHeader"
style="#style/AppTheme.Label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#string/dashboard_screen_reservation_card_headline"
android:textColor="#color/dashboard_reservation_card_text_color" />
</com.sinnerschrader.motelone.screens.dashboard.ReservationCard>
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
And here is my custom ConstraintLayout (ReservationCard):
class ReservationCard : androidx.constraintlayout.widget.ConstraintLayout {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private val gestureDetector: GestureDetector
init {
gestureDetector = GestureDetector(GestureListener())
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
if (ev?.action == MotionEvent.ACTION_DOWN) {
gestureDetector.onTouchEvent(ev)
return true
} else if (ev?.action == MotionEvent.ACTION_UP) {
gestureDetector.onTouchEvent(ev)
return false
} else {
super.onTouchEvent(ev)
return false
}
}
private inner class GestureListener : GestureDetector.OnGestureListener {
override fun onFling(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
return false
}
override fun onScroll(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
return false
}
override fun onLongPress(p0: MotionEvent?) {
}
override fun onDown(p0: MotionEvent?): Boolean {
return false
}
override fun onShowPress(p0: MotionEvent?) {
}
override fun onSingleTapUp(p0: MotionEvent?): Boolean {
return false
}
}
}

Binary XML file line #14: Error inflating custom ViewGroup class

Problem with inflating custom ViewGroup.
Unable to start activity
Binary XML file line #14: Binary XML file line #14: Error inflating class com.example.interactiveplatform.view.GraphLayout.
I searched about this problem and there is something wrong with constructors.
<LinearLayout
android:id="#+id/graph"
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity">
<com.example.interactiveplatform.view.GraphLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.example.interactiveplatform.view.GraphLayout>
</LinearLayout>
class GraphLayout(context: Context, attrs: AttributeSet, var chapterAdapter: ChapterAdapter) : ViewGroup(context, attrs) {
private val detector: GestureDetector
private val mMoveAndScaleHandler: MoveAndScaleHandler
private val myListener: GestureDetector.SimpleOnGestureListener
private var spacing = 0
init {
myListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean {
return true
}
}
detector = GestureDetector(context, myListener)
mMoveAndScaleHandler = MoveAndScaleHandler(context, this, chapterAdapter)
}
override fun onLayout(p0: Boolean, p1: Int, p2: Int, p3: Int, p4: Int) {
for(i in 0 until childCount){
val v = getChildAt(i)
v.layout(v.left, v.top, v.right, v.bottom)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
detector.onTouchEvent(event)
invalidate()
return mMoveAndScaleHandler.onTouchEvent(event)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(400 * chapterAdapter.numberChapters,chapterAdapter.heightScreen)
}
}
When you instantiate your custom view via XML you cannot pass custom constructor parameters like chapterAdapter. Also, you have to support all Android's view constructors. For example, you can do it like this:
class GraphLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr)

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

Categories

Resources