Search code examples
javaandroiddata-bindingandroid-databinding

BindingAdapter with lambda as argument


With Android databinding, I can do the following:

<View
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:onClick="@{() -> viewModel.onClick()}" />

My ViewModel does not have to implement OnClickListener, but just have a method:

public void onClick() {

}

What it passes to the onClick attribute in the Xml looks like a lambda to me.

How can I do this with my own BindingAdapters?

What I want: Let's assume I want to bind touch events and I want to pass the MotionEvent, I would imagine this to look in the Xml like this:

<View
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:onTouch="(view, event) -> viewModel.onTouch(event)" />

and the BindingAdapter something like:

@BindingAdapter("onTouch")
public static void onTouch(View view, ??? lambda) {
  view.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(final View v, final MotionEvent event) {
      return lambda(event);
    }
  });
}

I do not want my ViewModel to implement OnTouchListener and bind it like:

@BindingAdapter()
public  static  void onTouch(View view, View.OnTouchListener onTouchListener) {
    view.setOnTouchListener(onTouchListener);
}

and I do not want bind the touch event directly to my ViewModel like:

@BindingAdapter()
public static void onTouch(final View view, final MyViewModel myViewModel) {
  view.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(final View v, final MotionEvent event) {
      return myViewModel.onTouch(view, myViewModel);
    }
  });
}

Is this possible with Databinding?


Solution

  • As you want it to work like the built-in onClick, let's take a look at the official binding adapter for onClick attribute:

    @BindingAdapter({"android:onClick", "android:clickable"})
    public static void setOnClick(View view, View.OnClickListener clickListener, boolean clickable) {
        view.setOnClickListener(clickListener);
        view.setClickable(clickable);
    }
    

    As you can see, the binding adapter doesn't take a lambda as argument, it takes a listener. But you can pass a lambda to it in xml (since it is a single method interface)

    So here is the binding adapter you need: (I chose another attribute name, since onTouch is already used by framework)

    @BindingAdapter("onImageTouch")
    public static void onImageTouch(View view, View.OnTouchListener onTouchListener) {
        view.setOnTouchListener(onTouchListener);
    }
    

    This is how you use it in xml:

    app:onImageTouch="@{(view, event) -> viewModel.onImageTouch(event)}"
    

    And here is the corresponding method in viewmodel:

    public boolean onImageTouch(MotionEvent event){
        Timber.e("OnImageTouch is called");
        return true;
    }
    

    (It does nothing, but notice that it should return a boolean because onTouch callback returns a boolean.)

    You don't need to implement OnTouchListener in your viewmodel or activity. This works very similar to the example with onClick that you gave in the beginning.

    Let me note that there is already a built-in binding adapter for onTouch attribute, but I guess it was just an example and you in fact want to learn how to make custom ones. But be careful not to choose attribute names that already exist in the framework, to avoid any clashes.