Search code examples
androidandroid-databindingandroid-binding-adapter

Android BindingAdapter order of execution?


I need to understand how the Data Binding Library determine the order of execution for its BindingAdapters. If I have two BindingAdapters for a View and if the View has both the attributes corresponding to those BindingAdapters, how will it determine which adapter will be executed first? I ask because the order of execution matters in my case.

I have the following BindingAdapter/s:

public class SpinnerBindingAdapter {

    @BindingAdapter(value = {"entries"})
    public static void setEntries(Spinner spinner, List<? extends SpinnerItem> spinnerItems) {

        for (int i = 0; i < spinnerItems.size(); i++) {
            spinnerItems.get(i).setIndex(i);
        }
        ArrayAdapter<? extends SpinnerItem> adapter =
                new ArrayAdapter<>(spinner.getContext(), R.layout.spinner_item, spinnerItems);
        spinner.setAdapter(adapter);
    }

    @InverseBindingAdapter(attribute = "selectedItem", event = "selectedItemAttrChanged")
    public static Object getSelectedItem(Spinner spinner) {

        Object selectedItem = spinner.getSelectedItem();

        return selectedItem;
    }

    @BindingAdapter(value = {"selectedItem"})
    public static void setSelectedItem(Spinner spinner, SpinnerItem spinnerItem) {
        if (spinner.getAdapter() == null) {
            return;
        }
        // Other code omitted for simplicity
    }

    @BindingAdapter(value = {"selectedItemAttrChanged"}, requireAll = false)
    public static void setOnItemSelectedListener(Spinner spinner, final InverseBindingListener selectedItemChange) {
        if (selectedItemChange == null) {
            spinner.setOnItemSelectedListener(null);
        } else {
            spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

                @Override
                public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                    selectedItemChange.onChange();
                }

                @Override
                public void onNothingSelected(AdapterView<?> parent) {

                }
            });
        }
    }
}

And here is how I populate the Spinner and set the selection:

<Spinner
    android:id="@+id/spinner_system_activity_edit_tracker_unit"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="fill_horizontal"
    app:entries="@{DatabaseModel.queryForAll()}"
    app:selectedItem="@={object.selectedItem}"/>

DatabaseModel.queryForAll is a static method that queries the database and returns a list of objects which is then given to the BindingAdapter. The BindingAdapter takes this list, update each of its item with an index and set it as an adapter for the spinner.

For whatever reason, the "setSelectedItem" BindingAdapter always gets called first. This is undesirable, because I need the entries to be initialised first. If its not initialised first then spinner.getAdapter() will be null when setSelectedItem is first called. Which means that previous saved selection will not be restored.


Solution

  • There is no guaranteed order of execution in Android Data Binding. Because of this, you should merge binding adapters that have reliance on multiple attributes. In your case, you need to merge the binding adapter for selectedItem and entries.

    @BindingAdapter(value = {"selectedItem", "entries"}, requireAll = false)
    public static void setSelectedItem(Spinner spinner, SpinnerItem spinnerItem,
            List<? extends SpinnerItem> spinnerItems) {
        // Set entries attribute when provided
        if (spinnerItems != null) {
            for (int i = 0; i < spinnerItems.size(); i++) {
                spinnerItems.get(i).setIndex(i);
            }
            ArrayAdapter<? extends SpinnerItem> adapter =
                new ArrayAdapter<>(spinner.getContext(), R.layout.spinner_item, spinnerItems);
            spinner.setAdapter(adapter);
        }
        // set selectedItem attribute when provided
        if (spinnerItem != null) {
            if (spinner.getAdapter() == null) {
                return;
            }
            // Other code omitted for simplicity
        }
    }