50

I am using the new android data binding and it works great. I am able to perform data binding using EditText, TextView, Radio and checkbox. Now, I am not able to do the databinding in spinner.

Found some clue in below link: Android spinner data binding with xml layout

But, still not able to find the solution. Also need to perform the two way databinding. Should reflect the spinner data selected value.

Can someone please show me with an example?

Here is my xml code:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/tools"
    xmlns:card_view="http://schemas.android.com/apk/res-auto">

    <data>
        <import type="android.view.View" />
        <variable
            name="viewModel"
            type="com.ViewModels.model" />
    </data>

     <Spinner
                    android:id="@+id/assessmemt_spinner"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:layout_margin="@dimen/carview_margin"
                    android:layout_toRightOf="@+id/text_bp"
                    android:drawSelectorOnTop="true"
                    android:spinnerMode="dropdown"
                   android:visibility="@{viewModel.type.equals(@string/spinner_type)?  View.VISIBLE : View.GONE}" />
</layout>

View Model:

 public class AssessmentGetViewModel {
    private String valueWidth;
    private ArrayList<String> values;
    private String type;
    public String getValueWidth() { return this.valueWidth; }
    public void setValueWidth(String valueWidth) { this.valueWidth = valueWidth; }
    public ArrayList<String> getvalues() { return this.values; }
    public void setvalues(ArrayList<String> values) { this.values = values; }
    public String gettype() { return this.type; }
    public void settype(String type) { this.type = type; }
    }
Community
  • 1
  • 1
San Jaisy
  • 15,327
  • 34
  • 171
  • 290

7 Answers7

42

1 Line Solution

android:selectedItemPosition="@={item.selectedItemPosition}"

That's it! No need to make custom BindingAdapter.

Spinner already supports two-way binding by attributes selection and selectedItemPosition. See Android Documentation

You just need to use two way binding selectedItemPosition so that change on spinner reflect on your model field.

Example

Item.class

public class Item extends BaseObservable {
    private int selectedItemPosition;

    @Bindable
    public int getSelectedItemPosition() {
        return selectedItemPosition;
    }

    public void setSelectedItemPosition(int selectedItemPosition) {
        this.selectedItemPosition = selectedItemPosition;
        notifyPropertyChanged(BR.selectedItemPosition);
    }
}

activity_main.xml

<variable
    name="item"
    type="com.sample.data.Item"/>

<android.support.v7.widget.AppCompatSpinner
    ...
    android:entries="@array/items"
    android:selectedItemPosition="@={item.selectedItemPosition}"
    >

MainActivity.java

public class MainActivity extends AppCompatActivity {
    ActivityMainBinding binding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setItem(new Item());
        binding.getItem().setSelectedItemPosition(4); // this will change spinner selection.
        System.out.println(getResources().getStringArray(R.array.items)[binding.getItem().getSelectedItemPosition()]);
    }
}

If you need to get selected item from your code any time, then use this

binding.getItem().getSelectedItemPosition(); // get selected position
getResources().getStringArray(R.array.items)[binding.getItem().getSelectedItemPosition()]) // get selected item

Make your variable @Bindable if you need to programmatically change it.

binding.getItem().setSelectedItemPosition(4);

Otherwise you can remove @Bindable and notifyPropertyChanged(BR.selectedItemPosition);.

You can use any of BaseObservable or ObservableField or Live Data. It is up to you. I use BaseObservable because it is very simple., just extend from BaseObservable and all fields are observable now.

Khemraj Sharma
  • 57,232
  • 27
  • 203
  • 212
  • according to the question this answer is more relevant and simple. It should get higher vote. – Md Jubayer Apr 10 '19 at 10:05
  • @Khemraj can you explain why Bindable should be use? – Paweł Feb 21 '20 at 09:56
  • @Paweł You don't have to if you use LiveData or MutableLiveData – hr0m Apr 05 '20 at 09:12
  • 4
    But android:selectedItemPosition="@={viewModel.selectedItemPosition}" is showing a warning "Unknown attribute android:selectedItemPosition" – karan_for_you May 04 '20 at 07:48
  • @Karan_powered_by_RedBull it not even error this is ERROR, and according to documentation we should use Spinner not AppCompatSpinner – Roman Vasilyev May 08 '20 at 07:24
  • https://stackoverflow.com/questions/38736423/appcompatspinner-vs-android-widget-spinner-for-app-with-min-sdk-version-14 – Roman Vasilyev May 08 '20 at 07:30
  • This solution works even though you still get the warning `Unknown Attribute`. Just don't forget the `=` between `@` and `{` like me :/ – mcy Sep 28 '20 at 09:33
39

I found somethings might be helpful but it is not in the official documentation for the two-way data binding.

1. '@=' usage for the two-way data binding

2. Two-way custom data binding needs "BindingAdapter" and "InverseBindingAdapter" annotation to achieve this.

For the first item, lots of blogger showed the usage of "@=" for two way data binding. https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/

For the second item, as @George Mound replied here (Edit text cursor resets to left when default text of edittext is a float value) the EditText can be bind in two-way using "BindingAdapter" and "InverseBindingAdapter" annotation.

Following the instructions, you can build up your two-way binding method for spinner.

Firstly, create your ViewModel or use Pojo

ViewModel

public class ViewModel {
    private ObservableField<String> text;
    public ViewModel() {
        text = new ObservableField<>();
    }
    public ObservableField<String> getText() {
        return text;
    }
}

Pojo

public class ViewModel {
    private String text;
    public String getText() {
        return text;
    }

    public void setText(String text)
    {
       this.text = text;
    }
}

Secondly, add it into your xml.

  <android.support.v7.widget.AppCompatSpinner
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:entries="@array/days"
            bind:selectedValue="@={viewModel.text}"/>

Thirdly, add your bindingUtil

public class SpinnerBindingUtil {

    @BindingAdapter(value = {"selectedValue", "selectedValueAttrChanged"}, requireAll = false)
    public static void bindSpinnerData(AppCompatSpinner pAppCompatSpinner, String newSelectedValue, final InverseBindingListener newTextAttrChanged) {
        pAppCompatSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                newTextAttrChanged.onChange();
            }
            @Override
            public void onNothingSelected(AdapterView<?> parent) {
            }
        });
        if (newSelectedValue != null) {
            int pos = ((ArrayAdapter<String>) pAppCompatSpinner.getAdapter()).getPosition(newSelectedValue);
            pAppCompatSpinner.setSelection(pos, true);
        }
    }
    @InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged")
    public static String captureSelectedValue(AppCompatSpinner pAppCompatSpinner) {
        return (String) pAppCompatSpinner.getSelectedItem();
    }

}

As your saw, it used "selectedValue" as variable for the default selected value, but what is "selectedValueAttrChanged" ?? I thought this one is tricky (I don't know, why it is not null when it is called) , it is not need to be added in the xml since it is only the callback for listening the item changed in the spinner. And then you set the onItemSelectedListener and set it to call InverseBindingListener onchange() function (Documentation and example here : https://developer.android.com/reference/android/databinding/InverseBindingAdapter.html) The default event will be "android:textAttrChanged" and if you want to have custom two-way bind inversebind, you need to use the attribute with suffix "AttrChanged"

The default value for event is the attribute name suffixed with "AttrChanged". In the above example, the default value would have been android:textAttrChanged even if it wasn't provided.

Finally, in your activity and your string.xml

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityMainBinding lBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.activity_main, null, false);
    mViewModel = new ViewModel();
    mViewModel.getText().set("Wednesday");
    lBinding.setViewModel(mViewModel);
    lBinding.setHandler(new Handler());
    setContentView(lBinding.getRoot());
}

string.xml

<array name="days">
    <item name="Mon">Monday</item>
    <item name="Tue">Tuesday</item>
    <item name="Wed">Wednesday</item>
</array>

When you run the code, it will show "Wednesday" as the default value for the spinner.

peterh
  • 11,875
  • 18
  • 85
  • 108
Long Ranger
  • 5,888
  • 8
  • 43
  • 72
  • 3
    I get a NullPointerException from this, newTextAttrChanged is null even if you said it is not – Daniele Segato Oct 14 '16 at 16:18
  • 1
    ^^ probably you are missing the "=" after "@" in your xml. It should look like `bind:selectedValue="@={viewModel.text}"` – Sandip Fichadiya Nov 03 '17 at 14:22
  • thanks for your response, i am using the same code but i have a problem with initialization, I have a selectedValue by default that my spinner should select by default, in the first place bind:selectedValue doesn't contain the default value until a second call – Ghizlane Lotfi Jun 28 '19 at 15:15
  • There is a problem when trying to add another OnItemSelectedListener with this approach because it won't work. Any ideas of how to overcome this? – Chisko Aug 24 '19 at 23:54
  • Same here, compile error generated "Cannot find a getter that accepts parameter type 'java.lang.String'" – chitgoks Oct 07 '19 at 03:26
37

You can do it simple way with use onItemSelected and get selected position and selected item text.

1) add onItemSelected attribute to your spinner like below:

<Spinner
      android:id="@+id/spinner"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:entries="@array/item_list"
      android:onItemSelected="@{(parent,view,pos,id)->viewModel.onSelectItem(parent,view,pos,id)}"/>

2) now you need to add this method to your viewModel:

public void onSelectItem(AdapterView<?> parent, View view, int pos, long id)
{
    //pos                                 get selected item position
    //view.getText()                      get lable of selected item
    //parent.getAdapter().getItem(pos)    get item by pos
    //parent.getAdapter().getCount()      get item count
    //parent.getCount()                   get item count
    //parent.getSelectedItem()            get selected item
    //and other...
}

array could be somethings like this must save to values/item_list.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="item_list">
        <item>item1</item>
        <item>item2</item>
        <item>item3</item>
    </array>
</resources>

When the layout is drawn, onItemSelected is invoked , then you can set initial value:

parent.setSelection(1); //1 is position of initializing value
  • 9
    If using Kotlin, it's enough to just add android:onItemSelected="@{viewModel::onSelectItem}" in the XML, you don't need the full function declaration :) – Benjamin Menrad Jul 16 '19 at 10:52
  • @BenjaminMenrad, your suggestion is really good, it worked. Thanks, man!!! – Bhavnik Jan 05 '23 at 07:03
1

For me, https://stackoverflow.com/a/50338894/6791222 this solution worked perfectly. As mentioned in the solution, you should bind android:selectedItemPosition attribute. You can just copy paste the following in your view model and see the magic work.

@BindingAdapter("android:selectedItemPosition")
public static void setSelectedItemPosition(AdapterView view, int position) {
    if (view.getSelectedItemPosition() != position) {
        view.setSelection(position);
    }
}

@BindingAdapter(value = {"android:onItemSelected", "android:onNothingSelected",
        "android:selectedItemPositionAttrChanged" }, requireAll = false)
public static void setOnItemSelectedListener(AdapterView view, final OnItemSelected selected,
        final OnNothingSelected nothingSelected, final InverseBindingListener attrChanged)         
{
    if (selected == null && nothingSelected == null && attrChanged == null) {
        view.setOnItemSelectedListener(null);
    } else {
        view.setOnItemSelectedListener(
                new OnItemSelectedComponentListener(selected, nothingSelected, attrChanged));
    }
}

@BindingAdapter("android:selectedValueAttrChanged")
public static void setSelectedValueListener(AdapterView view,
        final InverseBindingListener attrChanged) {
    if (attrChanged == null) {
        view.setOnItemSelectedListener(null);
    } else {
        view.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                attrChanged.onChange();
            }
            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                attrChanged.onChange();
            }
        });
    }
}

@BindingAdapter("android:selectedValue")
public static void setSelectedValue(AdapterView<?> view, Object selectedValue) {
    Adapter adapter = view.getAdapter();
    if (adapter == null) {
        return;
    }
    // I haven't tried this, but maybe setting invalid position will
    // clear the selection?
    int position = AdapterView.INVALID_POSITION;

    for (int i = 0; i < adapter.getCount(); i++) {
        if (adapter.getItem(i) == selectedValue) {
            position = i;
            break;
        }
    }
    view.setSelection(position);
}

And in your xml just use android:selectedItemPosition="@={viewModel.value}"

Feroz Khan
  • 2,396
  • 5
  • 20
  • 37
0

@Long Ranger I really like your answer, but i think there is something we need to do to break the loop.like this:

@BindingAdapter(value = {"bind:selectedValue", "bind:selectedValueAttrChanged"}, requireAll = false)
public static void bindSpinnerData(AppCompatSpinner pAppCompatSpinner, final String newSelectedValue, final InverseBindingListener newTextAttrChanged) {
    pAppCompatSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
            if(newSelectedValue != null && newSelectedValue.equals(parent.getSelectedItem())){
               return;
            }
            newTextAttrChanged.onChange();
        }
        @Override
        public void onNothingSelected(AdapterView<?> parent) {
        }
    });
    if (newSelectedValue != null) {
        int pos = ((ArrayAdapter<String>) pAppCompatSpinner.getAdapter()).getPosition(newSelectedValue);
        pAppCompatSpinner.setSelection(pos, true);
    }
}
Khemraj Sharma
  • 57,232
  • 27
  • 203
  • 212
lulalagulu
  • 139
  • 5
0

Data binding provides us with built in support for two way binding with selectedItemPosition attribute for an AdapterView.

What worked for me was using my final store value as a MediatorLivedata and adding the selectedItemPosition livedata as a source. Each time the index was changed I would then publish the value to the mediator source. Below is how I have implemented it:

Spinner in XML Layout:

<androidx.appcompat.widget.AppCompatSpinner
    android:id="@+id/state"
    android:layout_width="@dimen/no_size"
    android:layout_height="@dimen/no_size"
    android:selectedItemPosition="@={viewModel.selectedItem}" />

Livedata in viewmodel:

val selectedItem = MutableLiveData<Int>() // This livedata captures selected item

val state = MediatorLiveData<String>()    // This is where I want to store the state value

init {
    state.addSource(selectedItem) {
        state.value = app.resources.getStringArray(R.array.states)[it]
    }
}

Angad Singh
  • 1,032
  • 1
  • 17
  • 36
-1

See my answer there to achieve simplest data binding with spinner. Indeed we need an Adapter to do further tasks.

XML code is there.

Java:


    //  Data binding
        ActivityParentsRegBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_parents_reg);
        binding.setCities(ConstData.getCitiesList());
ForWebTech
  • 140
  • 1
  • 13