Databinding in custom ViewGroup give error - android

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?

Related

Button is visiable but TextView / ImageView is not visiable in custom compound view

I am trying to create compound CardView using two CardView and extended Framelayout. If I place a button inside custom CardView as a child the button is visible but if i place TextView or ImageView as a child the widget (TextView or ImageView) is not visible.
Screenshot of Custom widget with TextView as child
Screenshot of Custom Widget with Button as child
kotlin code
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
class CompoundCardView : FrameLayout{
private var childView:CardView? = null
private var heading:String? = null
set(value) {
field = value
invalidate()
requestLayout()
}
get() {
return field
}
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context,
attrs: AttributeSet,
defStyle: Int) : super(context, attrs, defStyle) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
context.theme.obtainStyledAttributes(attrs,R.styleable.CompoundCardView,0,0).apply {
try {
heading = getString(R.styleable.CompoundCardView_heading)
}finally {
recycle()
}
}
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.compaund_card_view,this,true)
childView = findViewById(R.id.holder)
val header:TextView = findViewById(R.id.header)
header.text = heading
}
override fun addView(child: View?) {
invalidate()
childView?.addView(child,0, child?.layoutParams)
requestLayout()
}
}
xml layout
<?xml version="1.0" encoding="utf-8"?>
<merge 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.google.android.material.card.MaterialCardView
android:id="#+id/holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:background="#00FFFFFF"
app:cardCornerRadius="8dp"
app:strokeColor="#2196F3"
app:strokeWidth="1dp" />
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:backgroundTint="#FFC107"
app:cardCornerRadius="8dp"
app:contentPaddingBottom="4dp"
app:contentPaddingLeft="8dp"
app:contentPaddingRight="8dp"
app:contentPaddingTop="4dp"
app:strokeColor="#2196F3"
app:strokeWidth="1dp">
<TextView
android:id="#+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="#string/heading"
android:textSize="20sp" />
</com.google.android.material.card.MaterialCardView>
</merge>

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!

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.

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

How to set visibility of Android compound view via databinding in Kotlin?

I have checked many answers to find my issue however I was not successful. I have an activity that holds a compound drawable.
<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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.my.profile.widgets.ProfileWidget
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
....
</LinearLayout>
</layout>
This is my ProfileWidget:
class ProfileWidget #JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
#Inject lateinit var viewModel: ProfileWidgetViewData
#Inject lateinit var viewActions: ProfileWidgetActions
private val binding: WidgetProfileBinding = DataBindingUtil.inflate(
LayoutInflater.from(context), R.layout.widget_profile, this, true)
// private val binding = WidgetProfileBinding.inflate(LayoutInflater.from(context), this, true)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setupDependencyInjection()
setupDataBinding()
viewActions.testUI()
}
private fun setupDependencyInjection() {
(context as ProfileActivity).getProfileComponent()?.inject(this)
}
private fun setupDataBinding() {
binding.viewModel = viewModel
}
}
This is its layout:
<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="viewModel"
type="com.my.profile.widgets.ProfileWidgetViewData" />
</data>
<LinearLayout
android:id="#+id/profilesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#FF0000"
>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="profile 1"
android:visibility="#{viewModel.textView_1.get() ? View.VISIBLE : View.INVISIBLE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="profile 2"
android:visibility="#{viewModel.textView_2.get() ? View.VISIBLE : View.INVISIBLE}"/>
</LinearLayout>
</layout>
Finally my ViewModel class supposed to make TextViews
visible/invisible.
interface ProfileWidgetViewData {
val textView_1: ObservableBoolean
val textView_2: ObservableBoolean
}
interface ProfileWidgetActions {
fun testUI()
}
class ProfileWidgetViewModelImpl : ProfileWidgetViewData, ProfileWidgetActions {
override val textView_1 = ObservableBoolean(false)
override val textView_2 = ObservableBoolean(false)
override fun testUI() {
setProfilesContainerVisibility(true)
setAddProfileContainerVisibility(true)
}
private fun setProfilesContainerVisibility(isVisible: Boolean) {
textView_1.set(isVisible)
}
private fun setAddProfileContainerVisibility(isVisible: Boolean) {
textView_2.set(isVisible)
}
}
Unfortunately I don't see anything wrong in above codes. When I launch
the app, those two TextView are Invisible although I have set them to be visible.
Check below is added or not in build.gradle(obviously you already added)
apply plugin: 'kotlin-kapt'
android {
dataBinding {
enabled = true
}
}
dependencies {
kapt "com.android.databinding:compiler:3.1.3"
}
And add below line in your xml file for visibility or invisible
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="profile 1"
android:visibility="#{safeUnbox(viewModel.textView_1) ? View.VISIBLE : View.INVISIBLE}"/>

Categories

Resources