layout – How bind Android DataBinding to Menu?-ThrowExceptions

Exception or error:

As it supports Data Binding menu in android?
I write this code, but error: “Error:(16, 26) No resource type specified (at ‘visible’ with value ‘@{item.visible}’).”

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
   <variable
        name="item"
        type="ru.dixy.ubiworkerchecklistsmobile.Models.Fact"/>
    <import type="android.view.View"/>
</data>
    <item
        android:id="@+id/compliteitem"
        android:title="mybutton"
        android:icon="@drawable/complite"
        android:visible="@{item.visible}"
        app:showAsAction="ifRoom"
         />
</menu>
How to solve:

“At the moment, data binding is only for layout resources, not menu resources”

But, the behaviour can be achieved with Observable.OnPropertyChangedCallback. First you need to define OnPropertyChangedCallback:

private final Observable.OnPropertyChangedCallback propertyChangedCallback = new Observable.OnPropertyChangedCallback() {
    @Override
    public void onPropertyChanged(Observable observable, int i) {
        getActivity().invalidateOptionsMenu();
    }
};

Assuming that you have a binding for Fact model in your fragment:

<variable
    name="item"
    type="ru.dixy.ubiworkerchecklistsmobile.Models.Fact"/>

Now you need to register propertyChangedCallback and unregister it when you are done with it:

@Override
public void onStart() {
    super.onStart();
    binding.getItem().addOnPropertyChangedCallback(propertyChangedCallback);
}

@Override
public void onStop() {
    super.onStop();
    binding.getItem().removeOnPropertyChangedCallback(propertyChangedCallback);
}

Now we are ready update your view state based on the Fact model:

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    super.onCreateOptionsMenu(menu, inflater);
    inflater.inflate(R.menu.menu_fact, menu);
}

@Override
public void onPrepareOptionsMenu(Menu menu) {
    super.onPrepareOptionsMenu(menu);
    menu.findItem(R.id.compliteitem).setVisible(binding.getItem().isVisible());
}

###

At the moment, data binding is only for layout resources, not menu resources.

###

I realize this is an old question, but I wanted to provide a solution so it can help others with the same problem. This can be achieved using an action view for the menu item. It requires a fair bit of code, but it’s an approach that uses MVVM and can be used for any data binding.

This is an example where the icon shows a count and changes the background if the count is greater than 0.

Define the menu item

menu/main.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/action_count"
        android:enabled="true"
        android:icon="@drawable/ic_menu_red_square"
        android:title="@string/count"/>
</menu>

Define the view model for the menu item.

public class CountMenuViewModel extends BaseObservable {
    @Bindable
    int count;

    public CountMenuViewModel() {}

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
        if (this.count < 0) {
            this.count = 0;
        }
        notifyPropertyChanged(BR.count);
    }

    @Bindable({"count"})
    public @DrawableRes int getBackground() {
        if (count > 0) {
            return R.drawable.ic_menu_blue_square;
        }
        return R.drawable.ic_menu_red_square;
    }

    @Bindable({"count"})
    public String getCountText() {
        if (count > 0) {
            return String.valueOf(count);
        }
        return null;
    }
}

Define a callback that will be implemented by the activity when the menu item is clicked.

public interface CountMenuActionCallback {
    void onCountMenuItemClicked();
}

Create a layout for the action view. The layout uses the view model class and set the text for the count and background. The callback interface is used for the OnClickListener for the action view.

layout/menu_action_count.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="data"
            type="com.botnerd.samplesapp.CountMenuViewModel"
            />
        <variable
            name="callback"
            type="com.botnerd.samplesapp.CountMenuActionCallback"
            />
    </data>
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="@{() -> callback.onCountMenuItemClicked()}"
        android:background="?android:attr/actionBarItemBackground">

        <ImageView
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:layout_margin="4dp"
            android:src="@{data.background}"
            tools:src="@drawable/ic_menu_red_square"
            />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@{data.countText}"
            tools:text="30"
            android:textSize="14dp"
            android:maxLines="1"
            android:textColor="@android:color/white"
            tools:ignore="SpUsage"/>
    </FrameLayout>
</layout>

Note that a custom binding adapter is used for the android:src attribute. This is a good adapter for setting ImageView src via data binding.

@BindingAdapter({"android:src"})
public static void setSrc(ImageView view, @DrawableRes int resId) {
    try {
        view.setImageDrawable(ContextCompat.getDrawable(view.getContext(), resId));
    } catch (Resources.NotFoundException e) {
    }
}

Lastly, inflate the menu and bind the action view layout in the activity.

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);

    MenuItem menuItemCount = menu.findItem(R.id.action_count);
    MenuActionCountBinding binding = MenuActionCountBinding.inflate(getLayoutInflater());
    binding.setData(mCountMenuViewModel);
    binding.setCallback(mCountMenuActionCallback);

    MenuItemCompat.setActionView(menuItemCount, binding.getRoot());
    MenuItemCompat.setShowAsAction(menuItemCount, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);

    return super.onCreateOptionsMenu(menu);
}

For completeness, here are all the files in the sample that are not defined above.

drawable/ic_menu_blue_square.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <padding android:bottom="4dp"
             android:left="4dp"
             android:right="4dp"
             android:top="4dp"/>
    <solid android:color="#000080"/>
    <corners android:radius="2dp"/>

</shape>

drawable/ic_menu_red_square.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <padding android:bottom="4dp"
             android:left="4dp"
             android:right="4dp"
             android:top="4dp"/>
    <solid android:color="#800000"/>
    <corners android:radius="2dp"/>

</shape>

layout/activity_main.xml

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

    <data>
        <variable
            name="callback"
            type="com.botnerd.samplesapp.MainActivityActionCallback"
            />
    </data>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.botnerd.samplesapp.MainActivity">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="-"
            android:onClick="@{() -> callback.onMinusClicked()}"
            android:layout_marginStart="79dp"
            app:layout_constraintBaseline_toBaselineOf="@+id/button2"
            tools:layout_constraintBaseline_creator="1"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintLeft_toLeftOf="parent"/>

        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="+"
            android:onClick="@{() -> callback.onPlusClicked()}"
            tools:layout_constraintTop_creator="1"
            android:layout_marginStart="25dp"
            android:layout_marginTop="97dp"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/button"/>
    </android.support.constraint.ConstraintLayout>
</layout>

MainActivityActionCallback.java

public interface MainActivityActionCallback {
    void onPlusClicked();
    void onMinusClicked();
}

MainActivity.java

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding mBinding;
    CountMenuViewModel mCountMenuViewModel;

    CountMenuActionCallback mCountMenuActionCallback = new CountMenuActionCallback() {
        @Override
        public void onCountMenuItemClicked() {
            Toast.makeText(MainActivity.this, "Count clicked!", Toast.LENGTH_SHORT)
                    .show();
        }
    };

    MainActivityActionCallback mActionCallback = new MainActivityActionCallback() {
        @Override
        public void onPlusClicked() {
            mCountMenuViewModel.setCount(mCountMenuViewModel.getCount() + 1);
        }

        @Override
        public void onMinusClicked() {
            mCountMenuViewModel.setCount(mCountMenuViewModel.getCount() - 1);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCountMenuViewModel = new CountMenuViewModel();

        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mBinding.setCallback(mActionCallback);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);

        MenuItem menuItemCount = menu.findItem(R.id.action_count);
        MenuActionCountBinding binding = MenuActionCountBinding.inflate(getLayoutInflater());
        binding.setData(mCountMenuViewModel);
        binding.setCallback(mCountMenuActionCallback);

        MenuItemCompat.setActionView(menuItemCount, binding.getRoot());
        MenuItemCompat.setShowAsAction(menuItemCount, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);

        return super.onCreateOptionsMenu(menu);
    }


}

Leave a Reply

Your email address will not be published. Required fields are marked *