Android ViewBinding with CustomView - android

I'd like to use try out the ViewBinding with custom view, for example:
MainActivity <=> layout_main.xml
MyCustomView <=> layout_my_custom_view.xml
layout_main.xml
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapplication.MyCustomView
android:id="#+id/custom_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
layout_my_custom_view.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Line1" />
<View
android:id="#+id/divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#2389bb" />
<TextView
android:id="#+id/line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Line2" />
</LinearLayout>
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: LayoutMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.customView.line1.text = "Hello"
binding.customView.line2.text = "World"
}
}
In my MainActivity, I can use the binding to find MyCustomView but I can't further find #id/line1 and #id/line2 in MyCustomView. In this case, is it possible to use ViewBinding only or do I have to use findViewById or Kotlin synthetic ??
Thanks in advance.

ViewDataBinding.inflate doesn't generate of child view accessor inside custom view.
thus, you can't touch line1(TextView) via only use ViewDataBinding.
If you don't want using findViewById or kotlin synthetic, MyCustomView also needs to apply ViewDataBinding. try as below.
CustomView
class MyCustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val binding =
CustomLayoutBinding.inflate(LayoutInflater.from(context), this, true)
val line1
get() = binding.line1
val line2
get() = binding.line2
}
MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
with(binding.customView) {
line1.text = "Hello"
line2.text = "World"
}
}

Another approach is to return the CustomView binding object.
class CustomView constructor(context: Context, attrs: AttributeSet?) :
ConstraintLayout(context, attrs){
private val _binding: CustomViewBinding = CustomViewBinding.inflate(
LayoutInflater.from(context), this, true)
val binding get() = _binding
}
And then in your Activity or Fragment:
binding.customView.binding.line1?.text = "Hello"
binding.customView.binding.line2?.text = "World"

I believe you can have setter in your custom view. Since ViewBinding generates binding class for your main layout, it should return you the CustomView class. Thus, you can use the setter you just wrote for changing the texts.

Related

How to inflate custom view with XML layout with use of ViewBinding?

I have layout in XML:
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/hsv"
android:layout_width="match_parent"
android:layout_height="92dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#555555"
android:orientation="horizontal"
android:paddingHorizontal="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</HorizontalScrollView>
And extended HorizontalScrollView as custom view definition:
class TopBubblesWidget(context: Context, attrs: AttributeSet? = null) : HorizontalScrollView(context, attrs) {
private var binding: FragmentBiometricTopBubblesBinding = FragmentBiometricTopBubblesBinding.inflate(LayoutInflater.from(context))
private var data: List<BubblesWidget.Data>? = null
override fun onFinishInflate() {
super.onFinishInflate()
binding.rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
private fun initView(data: List<BubblesWidget.Data>) {
binding.rv.adapter = TopBubblesAdapter(data)
}
fun updateData(data: List<BubblesWidget.Data>) {
initView(data)
}
}
The problem is that TopBubblesWidget is not inflated by the XML and I do not see the RecyclerView.
What am I doing wrong here?
I have a feeling this is what you are looking for. This is a sample code -
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PartyViewHolder {
return PartyViewHolder(
PartyListItemBinding.inflate(LayoutInflater.from(parent.context)),
viewModel
)
}
and if you want to inflate a custom view like a prompt, here's a sample code -
private fun logOutAndExit() {
val dialogBox = Dialog(requireContext())
val promptLogOutBinding = PromptLogOutBinding.inflate(layoutInflater)//declaration done here
dialogBox.apply {
setContentView(promptLogOutBinding.root)
window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setCancelable(false)
show()
}
in the top, my layout file is party_list_item while, in the bottom example, my layout is prompt_log_out
and this -
PartyListItemBinding.inflate(LayoutInflater.from(parent.context))
is how you inflate a custom layout

Getting NullPointerException: Missing required view with ID for a custom view

Steps to reproduce my problem
Create a custom view
class VideoTrimmerView(context: Context, attrs: AttributeSet) : View(context) {
private val backgroundPaint: Paint = Paint().apply {
color = Color.BLACK
}
override fun draw(canvas: Canvas?) {
super.draw(canvas)
canvas?.drawRect(Rect(0, 0, width, height), backgroundPaint)
}
}
Add <declare-styleable> resources to the project
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="VideoTrimmerView" />
</resources>
Add layout xml file activity_video_trimmer.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".VideoTrimmerActivity">
<com.udara.developer.myapp.videotrimmer.VideoTrimmerView
android:id="#+id/video_trimmer_view"
android:layout_width="match_parent"
android:layout_height="64dp"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="#+id/open_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Open Video"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Enable view binding in module level build.gradle
android {
buildFeatures {
viewBinding true
}
}
Use view bindings to access views
class VideoTrimmerActivity : AppCompatActivity() {
private lateinit var binding: ActivityVideoTrimmerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityVideoTrimmerBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
}
I get the following error when running the app
Caused by: java.lang.NullPointerException: Missing required view with ID: com.udara.developer.my_app:id/video_trimmer_view
The problem only happens when custom views are used. How to solve this issue?
I have invoked super constructor without attrs parameter.
Previous constructor version
class VideoTrimmerView(context: Context, attrs: AttributeSet) : View(context) {
}
After adding attrs parameter
class VideoTrimmerView(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
In the android documentation it just tells
To allow Android Studio to interact with your view, at a minimum you must provide a constructor that takes a Context and an AttributeSet object as parameters.
Now the problem is solved!

databinding on custom view causes type mismatch

I have a custom view (LoadingButton) and when linking it to a variable in my fragment via databinding, it causes the following error: Type mismatch: inferred type is View but LoadingButton was expected
But when i use findViewById it works perfectly fine. How can I use databinding in this case?
Apparently, this question needs more text as there is an error message in stackoverflow which says "It looks like your post ist mostly code; please add some more details", so I guess have to write some more details:
code:
[FRAGMENT] LoginFragment.kt
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
private lateinit var login: LoadingButton
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentLoginBinding.inflate(inflater, container, false)
binding.mainViewModel = mainViewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUi()
}
private fun initUi() {
login = binding.buttonLoginLogin // causes error
// login = requireActivity().findViewById(R.id.button_login_login) // doesn't cause error
}
[LAYOUT XML] fragment_login.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.myapp.LoadingButton
android:id="#+id/button_login_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
[CUSTOM VIEW XML] view_loading_button.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:clipChildren="false">
<ProgressBar
android:id="#+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="4dp"
app:layout_constraintTop_toTopOf="#id/button"
app:layout_constraintStart_toStartOf="#id/button"
app:layout_constraintBottom_toBottomOf="#id/button"
app:layout_constraintEnd_toEndOf="#id/button"
android:elevation="4dp"/>
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_margin="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
[CUSTOM VIEW CLASS] LoadingButton.kt
class LoadingButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) :
ConstraintLayout(context, attrs, defStyleAttr) {
private val button: Button
private val progressBar: ProgressBar
private var loading: Boolean = false
private var buttonText: String = ""
private var textColor: Int
init {
inflate(context, R.layout.view_loading_button, this)
button = findViewById(R.id.button)
progressBar = findViewById(R.id.progressBar)
context.theme.obtainStyledAttributes(attrs, R.styleable.LoadingButton, 0, 0).apply {
try {
loading = getBoolean(R.styleable.LoadingButton_loading, false)
buttonText = getString(R.styleable.LoadingButton_text).toString()
textColor = getInt(R.styleable.LoadingButton_textColor, 0)
button.setTextColor(textColor)
button.text = buttonText
button.background = getDrawable(R.styleable.LoadingButton_buttonBackground)
setLoading(loading)
} finally {
recycle()
}
}
}
fun setLoading(isLoading: Boolean) {
loading = isLoading
if (loading) {
this.isClickable = false
button.isClickable = false
buttonText = button.text.toString()
button.text = ""
progressBar.visibility = View.VISIBLE
} else {
this.isClickable = true
button.isClickable = true
button.text = buttonText
progressBar.visibility = View.GONE
}
invalidate()
requestLayout()
}
}
It seems like this is some kind of bug as the code compiles on other user's IDE.
A workaround that works for me is by casting it explicitely (despite the IDE protesting):
login = binding.buttonLoginLogin as LoadingButton

NPE Crash in databinding when change the hierarchy of customView

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
class MyCustomView #JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(
context,
attributeSet
) {
val binding = MylayoutBinding.inflate(
LayoutInflater.from(context),
this,
true
)
}
// mylayout.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<merge>
<FrameLayout
android:id="#+id/myChildContainer"
android:layout_width="100dp"
android:layout_height="100dp"/>
</merge>
</layout>
I am using my custom view in the form below.
// myactivity.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent">
<MyCustomView
android:id="#+id/myView"
android:layout_width="300dp"
android:layout_height="300dp">
<Button
android:id="#+id/myButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</MyCustomView>
</FrameLayout>
</layout>
In the example above, I want myButton to go into MyCustomView as a child of myChildContainer.
I followed the example from android: how to add children from an xml layout into a custom view and modified MyCustomView as below.
class MyCustomView #JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(
context,
attributeSet
) {
val binding = MylayoutBinding.inflate(
LayoutInflater.from(context),
this,
true
)
override fun onFinishInflate() {
super.onFinishInflate()
while (childCount > 1) {
val child = getChildAt(1)
val param = child.layoutParams
removeView(child)
binding.myChildContainer.addView(child, param)
}
}
}
This code is fine if I don't apply data binding.
The problem is app has crashed when I apply data binding.
internal class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<MyactivityBinding>(this, R.layout.myactivity)
binding.myButton.text = "Test Text" <-- crash
}
}
Caused by: java.lang.NullPointerException: binding.myButton must not be null
When I change the execution point of remove view and addview of my code from onFinishInflate to onAttachedToWindow, the error generally does not occur, but I have confirmed that it recurs intermittently, so I think this is not a solution.
Is there any way to add Child View to CustomView while keeping DataBinding?
I actually use multiple such views, so I don't want to call individual functions like myView.sortView() after setContentView.

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