83

The Android data binding guide discusses binding values within an activity or fragment, but is there a way to perform data binding with a custom view?

I would like to do something like:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.mypath.MyCustomView
        android:id="@+id/my_view"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>

</LinearLayout>

with my_custom_view.xml:

<layout>

<data>
    <variable
        name="myViewModel"
        type="com.mypath.MyViewModelObject" />
</data>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{myViewModel.myText}" />

</LinearLayout>

</layout>

While it appears possible to do this by setting custom attributes on the custom view, this would quickly become cumbersome if there's a lot of values to bind.

Is there a good way to accomplish what I'm trying to do?

Dave
  • 1,896
  • 2
  • 14
  • 15
  • Did you try binding the data in your MyCustomView class after inflating the custome view? – Meiyappan Kannappa Jan 14 '16 at 21:37
  • I tried a few things to accomplish this, but the binding functions seem to require information contained within an activity or fragment. Their examples didn't give a sample on how to accomplish this within a custom view. A brief code snippet on how to accomplish this would be very much appreciated. – Dave Jan 14 '16 at 22:12
  • FYI: I think xml layout files are named with underscore notation by convention. – IgorGanapolsky Jan 25 '16 at 20:45
  • Yep, corrected the file name for clarity. – Dave Jan 27 '16 at 05:08
  • This article explains the 2-way data-binding process with a custom view and custom attributes: https://medium.com/@douglas.iacovelli/custom-two-way-databinding-made-easy-f8b17a4507d2 – Ricardo Nov 21 '17 at 17:07

8 Answers8

108

In your Custom View, inflate layout however you normally would and provide a setter for the attribute you want to set:

private MyCustomViewBinding mBinding;
public MyCustomView(...) {
    ...
    LayoutInflater inflater = (LayoutInflater)
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mBinding = MyCustomViewBinding.inflate(inflater);
}

public void setMyViewModel(MyViewModelObject obj) {
    mBinding.setMyViewModel(obj);
}

Then in the layout you use it in:

<layout xmlns...>
    <data>
        <variable
            name="myViewModel"
            type="com.mypath.MyViewModelObject" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.mypath.MyCustomView
            android:id="@+id/my_view"
            app:myViewModel="@{myViewModel}"
            android:layout_width="match_parent"
            android:layout_height="40dp"/>

    </LinearLayout>
</layout>

In the above, an automatic binding attribute is created for app:myViewModel because there is a setter with the name setMyViewModel.

George Mount
  • 20,708
  • 2
  • 73
  • 61
  • Thanks! Part of the struggle I was having was from Android Studio needing to have its cache invalidated, and the project rebuilt several times in order to get rid of intermediary files and generate new binding files. It was hard to know what was going wrong with all the red. – Dave Jan 16 '16 at 02:20
  • 1
    Where does the type `MyCustomViewBinding` get created? – IgorGanapolsky Jan 25 '16 at 20:59
  • 3
    MyCustomViewBinding is generated via an annotation processor as part of the data binding framework. The xml file (Dave used MyCustomView.xml, but he really meant my_custom_view.xml, since case should to be all lower case for resource files) causes a default Binding class name of MyCustomViewBinding (camel cased, suffixed with Binding). You can also customize the Binding class name: http://developer.android.com/tools/data-binding/guide.html#custom_binding_class_names – George Mount Jan 26 '16 at 00:13
  • @GeorgeMount, thanks for the hint, saved me a bunch of time. Maybe there is a place for it in the official databinding guide? – Dmitry Gryazin Feb 22 '16 at 18:16
  • 2
    The solution described in http://stackoverflow.com/a/36068337/1369016 (also posted by George Mount) is the one that worked for me. – Bitcoin Cash - ADA enthusiast Jun 15 '16 at 20:22
  • 1
    We have mistakenly left `inflate(getContext(), R.layout.my_custom_view, this);` in the constructor which caused weird issues (we ended up with 2 views inflated instead of 1) – Muxa Jul 29 '16 at 04:55
  • Does `MyCustomView` extend View? – Chad Bingham Oct 11 '16 at 00:22
  • 12
    Use `LayoutInflater.from(context)`, it's much nicer - shorter and you don't have to do a type conversion. – Jan Kalfus Feb 24 '17 at 08:59
  • 16
    Also, remember to define and attach to root: `inflate(inflater, this, true)`. I needed it in my case to have the custom view visible. – bearlysophisticated Jul 24 '17 at 12:06
  • @GeorgeMount I tried this way, but `myViewModel` in custom view is always null because I set it on onCreate of activity, this time the custom view is rendered – ductran Aug 25 '18 at 05:58
  • 1
    A common error is forgetting to set the variable on the binding. Make sure to call `mBinding.setMyViewModel(new MyViewModel())` or however your model is created. The binding class doesn't create one, so you must set it. – George Mount Aug 26 '18 at 02:05
  • 1
    For some reason, I use `mBinding = DataBindingUtil.inflate(inflater, myLayoutId, this, true);` instead for success binding data – Sarah0050 Feb 14 '19 at 05:58
  • Ensure `setMyViewModel(@Nullable MyViewModelObject obj)`, if using Kotlin then include `var myViewModel:MyViewModelObject? = null` and underneath `get(){}`. You have to because there is no guarantee myViewModel is available from the very start. Kotlin complained the variable was set as non null having assigned a nullable value. – Juan Mendez Feb 14 '19 at 23:41
  • @GeorgeMount Can you share the xml for the custom view – tinto mathew Aug 19 '20 at 07:12
15

Data binding works even with merge only parent had to be "this" and attach to parent true.

binding = DataBindingUtil.inflate(inflater, R.layout.view_toolbar, this, true)
Marek Brezina
  • 159
  • 1
  • 2
14

First, don't do this if this custom view is already being <include> in another layout, such as activity etc. You'll just get an exception about the tag being unexpected value. The data binding already ran the binding on it, so you're set.

Did you try using onFinishInflate to run the bind? (Kotlin example)

override fun onFinishInflate() {
    super.onFinishInflate()
    this.dataBinding = MyCustomBinding.bind(this)
}

Keep in mind that if you use the binding in your view, it won't be able to be created programmatically, at least it would be very convoluted to support both even if you can.

androidguy
  • 3,005
  • 2
  • 27
  • 38
11

Following the solution presented by george the graphical editor in android studio was no longer able to render the custom view. The reason is, that no view is actually inflated in the following code:

public MyCustomView(...) {
    ...
    LayoutInflater inflater = (LayoutInflater)
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mBinding = MyCustomViewBinding.inflate(inflater);
}

I suppose that the binding handles the inflation, however the graphical editor did not like it.

In my specific use case I wanted to bind a single field and not an entire view model. I came up with (kotlin incoming):

class LikeButton @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    val layout: ConstraintLayout = LayoutInflater.from(context).inflate(R.layout.like_button, this, true) as ConstraintLayout

    var numberOfLikes: Int = 0
      set(value) {
          field = value
          layout.number_of_likes_tv.text = numberOfLikes.toString()
      }
}

The like button consists of an image and a text view. The text view holds the number of likes, which I want to set via data binding.

By using the setter for numberOfLikes as an attribute in the following xml, data binding automatically makes the association:

<views.LikeButton
  android:id="@+id/like_btn"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  app:numberOfLikes="@{story.numberOfLikes}" />

Further reading: https://medium.com/google-developers/android-data-binding-custom-setters-55a25a7aea47

Community
  • 1
  • 1
DarkLeafyGreen
  • 69,338
  • 131
  • 383
  • 601
6

Today, I want to use the dataBinding on my Custom View class. But I don't know how to create data binding to my class. so I search the answer on StackOverflow. Firstly I try the answer:

LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
BottomBarItemCustomViewBinding binding = BottomBarItemCustomViewBinding.inflate(inflater);

but, I found this is not working for my code

so I change another method:

LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
BottomBarItemCustomViewBinding binding = DataBindingUtil.inflate(inflater, R.layout.bottom_bar_item_custom_view, this, true);

It's working for me.

the complete code is: bottom_bar_item_custom_view.xml

<data>

    <variable
        name="contentText"
        type="String" />

    <variable
        name="iconResource"
        type="int" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center">

    <ImageView
        android:id="@+id/bottomBarItemIconIv"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginTop="2dp"
        android:src="@{iconResource}"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/bottomBarItemContentTv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="@{contentText}"
        android:textColor="@color/black"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/bottomBarItemIconIv" />


</androidx.constraintlayout.widget.ConstraintLayout>

BottomBarItemCustomView.java

public class BottomBarItemCustomView extends ConstraintLayout {

public BottomBarItemCustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context, attrs);
}

private void init(Context context, AttributeSet attrs) {
    //use dataBinding on custom view.
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    BottomBarItemCustomViewBinding binding = DataBindingUtil.inflate(inflater, R.layout.bottom_bar_item_custom_view, this, true);

    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomBarItemCustomView);
    int iconResourceId = typedArray.getResourceId(R.styleable.BottomBarItemCustomView_bottomBarIconResource, R.drawable.my_account_icon);
    binding.setIconResource(iconResourceId);

    String contentString = typedArray.getString(R.styleable.BottomBarItemCustomView_bottomBarContentText);
    if (contentString != null) {
        binding.setContentText(contentString);
    }

    typedArray.recycle();
}

hope is useful for you!

Jere Chen
  • 71
  • 1
  • 3
2

In Kotlin we can directly use ViewBinding:

class BenefitView(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) {

    init {
        val binding = BenefitViewBinding.inflate(LayoutInflater.from(context), this, true)
        val attributes = context.obtainStyledAttributes(attrs, R.styleable.BenefitView)
        binding.image.setImageDrawable(attributes.getDrawable(R.styleable.BenefitView_image))
        binding.caption.text = attributes.getString(R.styleable.BenefitView_text)
        attributes.recycle()

    }
}
Soft Code
  • 49
  • 5
0

There are some good answers on here already, but I wanted to offer what I believe to be the simplest.

Create your custom control with the layout tags surrounding it, just like any other layout. See the following toolbar for example. this gets used in each of the activity classes

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

<data>
    <variable name="YACustomPrefs" type="com.appstudio35.yourappstudio.models.YACustomPreference" />
</data>

<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/YATheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/colorPrimary"
            app:popupTheme="@style/YATheme.PopupOverlay"/>

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

Now this custom layout is a child of every Activity. You simply treat it as such in the onCreate binding setup.

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    binding.yaCustomPrefs = YACustomPreference.getInstance(this)
    binding.toolbarMain?.yaCustomPrefs = YACustomPreference.getInstance(this)
    binding.navHeader?.yaCustomPrefs = YACustomPreference.getInstance(this)

    binding.activity = this
    binding.iBindingRecyclerView = this
    binding.navHeader?.activity = this

    //local pointer for notify txt badge
    txtNotificationCountBadge = txtNotificationCount

    //setup notify if returned from background so we can refresh the drawer items
    AppLifeCycleTracker.getInstance().addAppToForegroundListener(this)

    setupFilterableCategories()
    setupNavigationDrawer()
}

Notice I set the children's content at the same time I do the parent and it is all done through dot notation access. As long as the files are surrounded with layout tags and you named them, it is simple to do.

Now if the custom class has it's own associated code inflation, then it can easily just do it's own binding in it's onCreate or constructor, but you get the picture. If you have your own class just throw the following in the constructor to match it's named binding class. It follows the name convention of the layout file pascal cased, so it's easy to find and auto fill.

    LayoutInflater inflater = (LayoutInflater)
    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mBinding = NameOfCustomControlBinding.inflate(inflater);

Hope that helps.

Sam
  • 5,342
  • 1
  • 23
  • 39
0

I faced the same issue when I am trying to add a child views to a LinearLayout inside my host fragment(Nested fragment UI/UX)

here is my solution

var binding: LayoutAddGatewayBinding? = null
binding = DataBindingUtil.inflate(layoutInflater, R.layout.layout_add_gateway,
            mBinding?.root as ViewGroup?, false)
binding?.lifecycleOwner=this
val nameLiveData = MutableLiveData<String>()
nameLiveData.value="INTIAL VALUE"
binding?.text=nameLiveData

Here mBinding is child fragment ViewDataBinding object and I have used nameLiveData for two-way databinding

Muhamed Riyas M
  • 5,055
  • 3
  • 30
  • 31