to sum up I am trying to change the src of ImageButtons inside my dialog_colors.xml file from MainActivity. However whatever I do I couldnt manage to change it. I tried the same code with ImageButtons inside my activity_main.xml but it doesnt work for buttons inside dialog_colors.xml file.
activity_main.xlm
<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=".MainActivity">
<include
android:id="#+id/dialog_colors"
layout="#layout/dialog_colors"/> ...
dialog_colors.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:id="#+id/ll_paint_colors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="#+id/red"
android:layout_width="40sp"
android:layout_height="40sp"
android:src="#drawable/pallet_normal"
android:background="#color/Red"
android:layout_margin="1sp"
android:tag="redTag"
android:clickable="true"
android:onClick="selectColor"
/>...
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
colorDialog=findViewById<LinearLayout>(R.id.dialog_colors)
firstRowColors=colorDialog.findViewById<LinearLayout>(R.id.ll_paint_colors)
secondRowColors=colorDialog.findViewById<LinearLayout>(R.id.ll_paint_colors2)
drawingView=findViewById<DrawingView>(R.id.drawingView)
pressedColor=secondRowColors[0] as ImageButton
pressedColor!!.setImageResource(R.drawable.pallet_pressed)
}...
I tried the same thing with TextViews etc too. It seems like I cannot change anything inside the dialog_colors.xml file.
Do not get confused about the firstRows, secondRows etc, there are many ImageButtons, it doesnt work for any of them.
I found a solution to define an attribute in the MainActivity.kt through activity_main.xml to content_main.xml (included layout). The key word here is DataBinding. The project is completely reproducible and I provide first Kotlin and at the very end the JAVA files.
KOTLIN:
To enable DataBinding you need to go to your build.gradle(Module) and add following code:
//...
dataBinding{
enabled true
}
//...
You define a container called DrawableContainer as a Kotlin class. There you define a Drawable called customDrawable.
Thus DrawableContainer.kt:
import android.graphics.drawable.Drawable
data class DrawableContainer(val customDrawable: Drawable)
Now we will define our MainActivity.kt which will bind our chosen Drawable and pass it through our Container (DrawableContainer).
Our MainActivity.kt:
import android.app.Activity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import com.example.imagebuttonexperiment.databinding.ActivityMainBinding
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.drawable = DrawableContainer(resources.getDrawable(R.drawable.my_image))
}
}
The missing parts are our XML files. The code below shows our content_main.xml. It contains a variable (in <data>) we will define named drawable. The type guides to our DrawableContainer. So this is the first bridge between our Container and our layout we will <include. In the ImageButton you can see that as android:src we refer over our variable to our Drawable in our Container. That's why android:src="#{drawable.customDrawable}".
Thus content_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="drawable"
type="com.example.imagebuttonexperiment.DrawableContainer" />
</data>
<ImageButton
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="10dp"
android:src="#{drawable.customDrawable}"/>
</layout>
Now it's important to build our second bridge. Yet, we have DrawableContainer -> content_main. This will be the bridge content_main -> MainActivity. Therefor we have our <data/> and variable defined again. As you can see in <include we bind:drawable the exact same variable which is in both XML files.
The missing piece in our puzzle is the activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<data>
<variable
name="drawable"
type="com.example.imagebuttonexperiment.DrawableContainer" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="#+id/include_content"
layout="#layout/content_main"
bind:drawable = "#{drawable}"/>
</LinearLayout>
</layout>
RESULT:
The final result is an ImageButton in the MainActivity that was included by another Layout. But the Drawable was chosen from the MainActivity and passed a Container to be shown in the included Layout. Magic isn't it!? ;)
JAVA:
And I will keep my word and provide you the JAVA version also.
The XML files and gradle will be the same, you only need to use MainActivity.java instead of MainActivity.kt, the same for DrawableContainer.java instead of DrawableContainer.kt.
Here is the same in green, DrawableContainer.java:
import android.graphics.drawable.Drawable;
public class DrawableContainer {
public final Drawable customDrawable;
public DrawableContainer(Drawable customDrawable) {
this.customDrawable = customDrawable;
}
}
And of course our MainActivity.java:
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import com.example.imagebuttonexperiment.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main);
mainBinding.setDrawable(new DrawableContainer(getDrawable(R.drawable.my_image)));
}
}
DOWNLOAD PROJECT:
In addition I provide the project with both, JAVA and Kotlin classes in it. You just need to comment the content of the 2 classes grey you don't want. In example, if you want to use Kotlin, grey the content of .java files.
Github Project
Documentation & Tutorial:
Databinding Android Documentation
Youtube Tutorial: How to bind data?
NOTE: That's somewhat a solution of my problem:
I have tried this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
colorDialog2=Dialog(this)
colorDialog2.setContentView(R.layout.dialog_colors)
colorDialog2.setTitle("Colors")
firstRowColors=colorDialog2.findViewById<LinearLayout>(R.id.ll_paint_colors)
secondRowColors=colorDialog2.findViewById<LinearLayout>(R.id.ll_paint_colors2)
drawingView=findViewById<DrawingView>(R.id.drawingView)
pressedColor= secondRowColors[0] as ImageButton
pressedColor!!.setImageResource(R.drawable.pallet_pressed)
}
intead of this in Main.Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
colorDialog=findViewById<LinearLayout>(R.id.dialog_colors)
firstRowColors=colorDialog.findViewById<LinearLayout>(R.id.ll_paint_colors)
secondRowColors=colorDialog.findViewById<LinearLayout>(R.id.ll_paint_colors2)
drawingView=findViewById<DrawingView>(R.id.drawingView)
pressedColor=secondRowColors[0] as ImageButton
pressedColor!!.setImageResource(R.drawable.pallet_pressed)}
And it worked, I managed to change the src however I wanted. (Only did this change didnt do the changes that the others provided) However, I am not even sure if this is a good practice. It works as a quick fix for sure tho.
I will look into DEX7RA's answer more later on.
My project contains multiple modules, and I am using aar files of other modules contains custom views and components. so I have an XML in .aar and I want to use it in my project.
Of course, I can but DataBinding doesn't generate it in the generated file, so I don't have access to XML's components and widgets.
My fragment's XML is:
<?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"
android:orientation="vertical">
<!-- Comes from AAR file -->
<include
android:id="#+id/toolbarLayout"
layout="#layout/simple_tool_bar" />
Simple toolbar is:
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="#dimen/simple_tool_bar_height"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationIconTint="#color/primary" />
</com.google.android.material.appbar.AppBarLayout>
And Databinding generated file is:
public abstract class FragmentTestBinding extends ViewDataBinding {
#NonNull
public final View toolbarLayout;
So as you can see in the generated file toolbarLayout is an instance of View but it should be SimpleToolBarBinding.
When you are working on a project that contains modules instead of .aar it everything is working well, but after generating the .aar file and use it in another project it doesn't.
Conclude:
The Android Gradle Plugin (version: 7.2.0) doesn't support to generate the matched type of XxxBinding when the layout file is in class (aar).
Reason:
I compared the gradle tasks of using module's layout file with using aar's layout file.
We missed following gradle tasks:
:module:dataBindingMergeDependencyArtifactsDebug
:module:dataBindingMergeGenClassesDebug
:module:dataBindingGenBaseClassesDebug
And analyzed the related ViewBinding AGP source code, in the function of android.databinding.tool.writer.ViewBinderKt#toViewBinder
// Check to make sure that the ID matches a binding. Ignored tags like <merge> or <fragment>
// might have an ID but not have an actual binding. Only use ID if a match was found.
val rootBinding = bindings.singleOrNull { it.id == id }
if (rootBinding != null) {
return RootNode.Binding(rootBinding)
}
If the binding doesn't in the binding list, it would be regarded as View.
Workaround
Using the function of ViewBinding.bind(view: View) to inflate the XxxBinding manually.
layout file
<!-- activity_awesome.xml -->
<androidx.constraintlayout.widget.ConstraintLayout>
<include android:id="#+id/includes" layout="#layout/included_buttons" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- included_buttons.xml 在aar中-->
<androidx.constraintlayout.widget.ConstraintLayout>
<Button android:id="#+id/include_me" />
</androidx.constraintlayout.widget.ConstraintLayout>
Generated Code
// Activity.kt
class Activity {
private val includedButtonsBinding :IncludedButtonsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val rootBinding = ActivityAwesomeBinding.inflate(inflater, container, false)
includedButtonsBinding = IncludedButtonsBinding.bind(ActivityAwesomeBinding.includes)
return rootBinding.root
}
}
The following sample code CameraFragment.kt and activity_main.xml is from camera-samples project.
It find the NavController using findNavController(requireActivity(), R.id.fragment_container).
I think it's a complex, can I always use findNavController(mView) to find the NavController ? just like Code A?
CameraFragment.kt
private fun updateCameraUi() {
...
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
CameraFragmentDirections.actionCameraToGallery(outputDirectory.absolutePath))
}
activity_main.xml
<FrameLayout
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:keepScreenOn="true"
tools:context=".MainActivity">
<fragment
android:id="#+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</FrameLayout>
Code A
private lateinit var mView: View
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mView=view
...
}
Navigation.findNavController(mView).navigate(
CameraFragmentDirections.actionCameraToGallery(outputDirectory.absolutePath))
You don't even need to use Navigation.findNavController(mView) when in a Fragment - you can use NavHostFragment.findNavController(this) to find the NavHostFragment from a Fragment as per the Navigate to a destination documentation.
But yes, you can also use Navigation.findNavController(mView) or use any View from within the Fragment.
You'd only want to use findNavController(requireActivity(), R.id.fragment_container) when you only have the Activity. While it works, there easier ways to do the same thing.
I am having trouble getting data-binding to work properly on a BottomSheetDialog layout. Here are the details:
Definition and setting of var:
private lateinit var myDrawerBinding: MyDrawerBinding
myDrawerBinding = MyDrawerBinding.bind(myDrawerContent) // crashes on this line
and later it's set and shown this way (although it never gets to this point)
myDrawerBinding.viewModel = theViewModel
val bottomSheet = BottomSheetDialog(context)
bottomSheet.setContentView(myDrawerBinding.myDrawerContent)
bottomSheet.show()
Here is a snippet of the XML layout (my_drawer.xml):
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable name="viewModel" type="path.to.my.viewModel"/>
</data>
<RelativeLayout
android:id="#+id/myDrawerContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<View
android:layout_width="50dp"
android:layout_height="3dp"
android:visibility="#{viewModel.shouldShowView() ? View.VISIBLE : View.GONE}"/>
....
The crash occurs when it calls the .bind() method above, and the error is:
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.view.View.getTag()' on a null object reference
This same exact functionality works on a separate DrawerLayout that I am showing in the same Fragment, but for some reason the BottomSheetDialog layout is giving problems.
Finally found a fix for this. I had to treat this view a little differently from my DrawerLayout, although I have a feeling this approach may work for that view as well.
Here is the binding setup:
val myDrawerView = layoutInflater.inflate(R.layout.my_drawer, null)
val binding = MyDrawerBinding.inflate(layoutInflater, myDrawerView as ViewGroup, false)
binding.viewModel = theViewModel
And then to show the view:
val bottomSheetDialog = BottomSheetDialog(context)
bottomSheetDialog.setContentView(binding.myDrawerContent)
bottomSheetDialog.show()
Works like a charm now!
I've two layouts for a screen. Activity works fine while setting a layout for Mobile device but it's causing error while setting layout for tablet device. The main issue is:
Caused by: java.lang.RuntimeException: view must have a tag at
com.mypackage.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:941)
Though, I don't face the problem when I install app on mobile device.
This way I'm setting layout on activity:
val resetPasswordActivityBinding = DataBindingUtil.setContentView<ResetPasswordActivityBinding>(this,
R.layout.reset_password_activity)
resetPasswordActivityBinding.resetPasswordViewModel = resetPasswordViewModel
Here is my XML layout for tablet screen:
<?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">
<data>
<variable
name="resetPasswordViewModel"
type="com.bhi.salesarchitect.user.password.reset.ResetPasswordViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="#+id/toolbar"
layout="#layout/app_toolbar_layout"
app:appTheme="#{resetPasswordViewModel.appTheme}"
app:appToolbar="#{resetPasswordViewModel.appToolbar}" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/bg_splash"
android:contentDescription="#null"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="#+id/imv_builder_logo_change_pswd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/dp_135"
android:layout_marginBottom="#dimen/space_xxlarge"
android:layout_weight=".5"
android:contentDescription="#null"
android:src="#drawable/ic_logo" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="#dimen/dp_80"
android:layout_weight=".4">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="#dimen/space_normal"
android:layout_marginEnd="#dimen/space_normal"
android:contentDescription="#null"
android:scaleType="fitXY"
android:src="#drawable/bg_white_shadow" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="#dimen/space_xxxlarge"
android:paddingEnd="#dimen/space_xxxlarge">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/dp_55"
android:layout_marginBottom="#dimen/space_small"
android:gravity="center"
android:text="#string/a_one_time_password_reset_code_has_been_sent_to_your_email"
android:textColor="#color/blue_dark_main"
android:textSize="#dimen/text_size_normal" />
<EditText
android:id="#+id/et_otp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/space_normal"
android:background="#drawable/shape_rounded_white"
android:drawablePadding="#dimen/space_small"
android:hint="#string/password_reset_code"
android:inputType="textPersonName"
android:paddingStart="#dimen/space_small"
android:paddingTop="#dimen/space_xsmall"
android:paddingEnd="#dimen/space_xxsmall"
android:paddingBottom="#dimen/space_xsmall"
android:textColor="#android:color/black"
android:textSize="#dimen/sp_15" />
<EditText
android:id="#+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/space_small"
android:background="#drawable/shape_rounded_white"
android:drawableStart="#drawable/ic_password"
android:drawablePadding="#dimen/space_small"
android:hint="#string/new_password"
android:inputType="textPassword"
android:maxLength="#integer/max_password_length"
android:paddingStart="#dimen/space_small"
android:paddingTop="#dimen/space_xsmall"
android:paddingEnd="#dimen/space_xxsmall"
android:paddingBottom="#dimen/space_xsmall"
android:textColor="#android:color/black"
android:textSize="#dimen/sp_15" />
<EditText
android:id="#+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/space_small"
android:background="#drawable/shape_rounded_white"
android:drawableStart="#drawable/ic_password"
android:drawablePadding="#dimen/space_small"
android:hint="#string/confirm_new_password"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLength="#integer/max_password_length"
android:paddingStart="#dimen/space_small"
android:paddingTop="#dimen/space_xsmall"
android:paddingEnd="#dimen/space_xxsmall"
android:paddingBottom="#dimen/space_xsmall"
android:textColor="#android:color/black"
android:textSize="#dimen/sp_15" />
<Button
android:id="#+id/bt_submit"
style="#style/ButtonNormal"
android:layout_marginTop="#dimen/space_normal"
android:backgroundTint="#color/blue_dark_main"
android:onClick="#{()-> resetPasswordViewModel.onSubmitClick()}"
android:text="#string/submit"
android:textColor="#android:color/white" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
</layout>
I ran into this when I had:
A library module defining a layout resource
An app module that depended upon that library module defining the same layout resource
The library layout resource was set up for data binding (e.g., root <layout> element), but the app module's edition of that layout resource was not
In my case, the app module's layout was left over from when I created the project. Removing it cleared up the problem.
Keeping two layouts, one with layout tag of data-binding and another without it, is the common cause of this issue.
I got stuck when I renamed my two normal layout files with same name (/layout and /layout-sw720dp) and used tag. Then, it worked for mobile device but not for tablet. So, after cleaning project, it all started working.
I was having this problem when using an array adapter, having a crash due to a missing tag on convertView. In my getView(), I was doing:
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
if (convertView == null) {
DataBindingUtil.inflate<ItemSpinnerDropDownWorkPackageFilterBinding>(
LayoutInflater.from(parent.context),
R.layout.item_spinner_drop_down_work_package_filter,
parent,
false
)
} else {
binding = ItemSpinnerDropDownWorkPackageFilterBinding.bind(convertView)
binding.text1.setText(getItem(position))
setDividerVisibility(binding.divider, position)
return convertView
}
}
Which was crashing. The solution was to set the tag on the first run through getView:
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = if (convertView == null) {
DataBindingUtil.inflate<ItemSpinnerDropDownWorkPackageFilterBinding>(
LayoutInflater.from(parent.context),
R.layout.item_spinner_drop_down_work_package_filter,
parent,
false
)
} else {
convertView.tag as ItemSpinnerDropDownWorkPackageFilterBinding
}
binding.text1.setText(getItem(position))
setDividerVisibility(binding.divider, position)
binding.root.tag = binding
return binding.root
}
Using viewStub with databinding:
class MyFragment {
lateinit var binding: MyFragmentBinding
lateinit var viewStubBinding: MyViewStubBinding
private fun setViewStub() {
binding.myViewStub.setOnInflateListener { viewStub, view ->
viewStubBinding = binding.myViewStub.binding as MyViewStubBinding // property viewStubBinding is finally inflated
}
binding.myViewStub.viewStub?.inflate() // inflate viewStub with defined layout file in XML
}
}
When I tried to use MyViewStubBinding.bind(view / viewStub) it crash on View must have a tag. This happen because ViewStubProxy try to inflate binding class by itself, so when I try to call bind() by myself on same Binding class, it was already bound and crashed with this error.
You need to add layout tag at start of your app_toolbar_layout layout file
You must have <layout> tag into your all XML views (portrait, landscape, tablet, etc.). Even you have to include <layout> tag into included views ("#layout/app_toolbar_layout").
i was facing similar error so my work around was
DataBindingUtil.bind(holder.itemView)?.apply
{
item = items[position]
}
I just needed to clean and rebuild after deleting a resource file.
May be you have multiple xml layout files.
if you already use layout tag in your xml file, and you still give error, just rename your layout file name and clean/rebuild the project again it will fix
I had this error trying to bind separately as my view is inflated within a library but I supply the layout.
The doc says you can do this:
val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot)
The viewRoot I was passing was derived from the custom class but ended up being a parent of my layout file (the one that included the layout tag). So I had to specify the view as the root of my layout file via findViewById
I encounter this problem, the root case is there are duplicate layout resource file in different module. delete the redundant one to resolve the issue.
In my case worked this code to set content view with data binding.
binding = GenericItemDetailsListBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());