Wednesday, October 28, 2009

Spinner and its data behind

I got another comment asking about Spinner and the data behind the widget. The author of the comment wanted to know, how to associate data to the item the Spinner displays. Then that associated data would be used to look up some other data source.

Click here to download the example program.

In order to understand the trick, one has to know, how Adapters work in general and ArrayAdapter in particular. Adapters are objects that are able to turn data structures into widgets. The widgets are then displayed in a List or in a Spinner. So the two questions an Adapter answers are:
  • What widget or composite view to associate with a data structure with a certain index.
  • How to extract data from the data structure and how to set field(s) of the widget or composite view according to this data.
ArrayAdapter's answers are:
  • Each widget for any index is the same and is inflated from the resource whose ID the ArrayAdapter receives in its constructor.
  • Each such a widget is expected to be an instance of TextView (or descendant). The widget's setText() method will be used with the string format of the item in the supporting data structure. The string format will be obtained by invoking toString() on the item.
Knowing these facts, the solution is very simple. We back ArrayAdapter with our own data structure. In this simple example MyData structure has just two fields but we can use structures with arbitrary complexity. The only thing we have to care about is that the data object's toString() method will be used to obtain the text which is displayed in the Spinner. Hence we made sure that toString() returns just one field of the object. The other field will be retrieved from MyData when the item is selected. Our application is written in such a way that then the other field is displayed in a separate text field.

19 comments:

Anonymous said...

Hi,

Great explanation!!!

I would also like to point out that instead of maintaining the array of MyData, you could take it out from the initialization of the adapter and add items after the initialization. You could then use the adapter's getItem method to get the selected item.

ArrayAdapter<MyData> adapter =
new ArrayAdapter<MyData>(
this,
android.R.layout.simple_spinner_item);

adapter.add(new MyData("key1", "value1"));
adapter.add(new MyData("key2", "value2"));
adapter.add(new MyData("key3", "value3"));

To retrieve the selected item in the onItemSelected method in the listener:

MyData d = adapter.getItem(position);

Thank you very much for your time. This has helped me a lot.

SunD

Amit said...

Thanks for explaining it. I need to do something little different.

I want spinner lable to always say a constant string say "More...". When you click on it, it needs to show array (as usual) and on select of any item in it, display "More..." on the screen instead of displaying label of selected item. How do I do this?

Gabor Paller said...

Hi, Amit,

The standard Spinner implementation does not allow you to do this because the Adapter backing the dropdown list is derived from the Adapter that is backing the Spinner view itself. This can be easily changed, however.

Create a custom view based on the platform's Spinner class.


Source of the Spinner class


Then modify the source in such a way that the adapter backing the Spinner and the adapter backing the dropdown list are separated.

Anonymous said...

setPrompt(CharSequence prompt) i think u may use it after perfoming click..

Meblin said...

This has helped me loads in my latest app createion.

How can I get the spinner to remember what it was set to once the app ic closed?

can I and something to my onDestroy()

Many thanks

Meblin

SkyDiver said...

Thanks for the help!

Just to finalize what Anonymous (SunD) wrote in the first message, the proper way to get the data in the event listener would be:

SpinnerData d = (MyData)parent.getAdapter().getItem(position);

Unknown said...

Thanks for the explanation. This is all very straight forward for these purposes, but I wonder if there isn't a way to accomplish this while still populating the spinner with items from an external XML? This question probably has more to do with basic java literacy than with the way a spinner works...

Gabor Paller said...

agitcraft, I am not sure I understand your question. If you want to populate the spinner from external XML, all you have to do is to create the data items behind the spinner from the XML file. Android has DOM, SAX and Pull parsers built in from API version 1 so this should not be a problem.

oren said...

To pick up where agitcraft left off, I think what we're looking for is a way to define such an array in the resources xml files (same as you would do with a string array for a "regular" spinner). I mean an array where the element type is MyData.
Does anyone have such an example?

bruno imbrizi said...

Exactly what I was looking for. Thanks for posting.

RAJAREDDY said...

hi,
this is very good tutorial for spinner , who have created his own data . this very much useful for me
very good.

Juan Pablo Solano said...

First Thanks a lot for this piece of code.

I have a question I would be very happy if someone can lead me to some directions. I am not a hardcore developer more of a designer.

The Q: I am working with a database and I have a spinner with some clients name; I show the client name as the "value" and the client database ID as the "key". I then store the "key" into some other table in the database in case the user changes the client's name.

When the user wants to modify the data I need to get from the number Id of the client the proper position in the spinner.

I was doing it fine with getPosition(String) but with this new Adapter you have I cannot get the position I always get a -1. I can not return to the normal ArrayAdapter because I need to be able to store the "key" and the "value" provided by your class.

What should i do?
Do I make myself clear. my english is not so good.

Thank you very much

Gabor Paller said...

Juan Pablo, getPosition's prototype is exactly:
int getPosition(T item)

This means that if you template ArrayAdapter, then you have to pass a MyData instance as getPosition input argument. That's not enough, you have to implement an equals() method in MyData. Read this tutorial about the equals() method..

When you implement equals() you have to decide when you consider two MyData objects equal. If I understand correctly, you want to search by database IDs so you should consider two MyData instances equals if their database ID fields are equal.

harish said...

After selecting spinner item entering into the next view but when we write onitem click listener for this first time when we run the app it'll automatically goes to the next view.

but it needs to go after we selecting the item how can i solve this problem.

Anonymous said...

there is a way to change the renderer of the spinner as it is in Java Swing? Do I necessary have to take care of the "toString" method or there is a more flexible way?

Gabor Paller said...

Anonymous, toString() is only relevant if you use ArrayAdapter. There are a bunch of other Adapters available and you can easily write your own.

cErEbRaL aSsAsSiN said...

Hey. this works good.. but is their any way to create multi-level spinner in android.... i.e. options within options in android

Anonymous said...

Thank you for your share, helpe me a lot.

Anonymous said...

Thank you very much, it is simple and very useful!

If someone need, as I did, to select programmatically the value of the spinner, not based on the index or value but based on the ID (from a database for example) you can do this (I am a beginner, not sure is the optimal way but it works):

Suppose you have on your database a table with 2 columns:
ID Language
11 Italian
12 Portuguese
13 Spanish
and you need to select the language from the spinner with ID = 12:

add:
private MyData lista_limbi[] = new MyData[0];
on the declarations of the class

change old values with:
lista_limbi = Arrays.copyOf(lista_limbi, lista_limbi.length + 4);
lista_limbi[0] = new MyData( "","" );
lista_limbi[1] = new MyData( "Italian", "11" );
lista_limbi[2] = new MyData( "Portuguese", "12" );
lista_limbi[3] = new MyData( "Spanish", "13" );

change
if (spinner.getItemAtPosition(i).toString().equalsIgnoreCase(myString))
whith
if (lista_limbi[i].getValue() == myString)

and then from the main program change the selected item:
Spinner mySpinner = (Spinner)findViewById(R.id.spinner_limbi);
mySpinner.setSelection(getIndex(mySpinner, "12"));

=> it will be selected the line with 'Portuguese'