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!
Related
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?
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
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.
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.
I'm including a layout passing a viewmodel:
<include
layout="#layout/user_login"
app:model="#{model}" />
My goal is to use this include in different places passing a different ViewModel too.
For example I would like to pass: UserViewModel in one place, in another place InformationViewModel.
Can I do that?
Thanks in advance
Here is the component class
class SomeComponent(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
private var mBinding: SomeLayoutBinding? = null
init {
if (isInEditMode) {
val view = LayoutInflater.from(context).inflate(R.layout.some_layout, this, false)
addView(view)
} else {
mBinding = SomeLayoutBinding.inflate(LayoutInflater.from(context), this, true)
}
}
fun setVm(vm: InformationViewModel) {
mBinding!!.vm = vm
mBinding!!.executePendingBindings()
}
}
Here is the layout for component
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="vm"
type="yourpackage.InformationViewModel" />
</data>
</layout>
Here is the way how you set vm
<yourpackage.SomeComponent
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:vm="#{item}" />