Material Spinner (Combo Box) for Android

Miloš Černilovský
Geek Culture
Published in
5 min readApr 21, 2021

--

Photo by Denny Müller on Unsplash

When it is necessary to implement a combo box in an Android app, most developers probably think it should be a piece of cake. Unfortunately, this is not the case, as with a lot of other issues we come across when implementing Android apps. In this article, I will show how to implement a custom Spinner with a Material theme. The result can look similar to this (depending on your styling, attributes, and item layout):

Terminology

First thing which can be a bit surprising is that there is no ComboBox class. Instead of using this very common terminology Google decided to use the Spinner keyword. This is even more confusing for people who have experience with programming desktop applications where Spinner is a UI element for picking a number.

After the initial shock you can think that the Spinner class will do. In simple apps where the looks are not so important and only the functionality is necessary, it can be a quick and efficient solution. However, as you can see below, the result doesn’t look very nice. Spinner doesn’t follow the Material guidelines and offers no room for customisation.

Spinner doesn’t look so nice. Switches are shown below the Spinner and are not its part, but it is difficult to distinguish.

If you check the Material design website, it mentions something called “Exposed dropdown menu” which uses the TextInputLayout and AutoCompleteTextView, making everything even more confusing. I think it is really weird the only official way for implementing a Spinner with Material theme is abusing a component whose original purpose is completely different. I’ve learned about these components from this excellent article and I give the author credit for the research. It also shows some details about manually styling the component. However, I have implemented the Spinner in a different, reusable way using custom binding adapters, two-way data binding and selection tracking.

Material Spinner

First, let’s create our new custom UI component called MaterialSpinner which extends AutoCompleteTextView. This component automatically tracks the position of the currently selected item by monitoring user clicks and notifies registered listeners. Later we will also create a custom adapter which automatically gets notified when the selection changes. If the selectedPosition is programmatically updated, the component automatically notifies listeners and updates the selected text. Lastly, it also supports emptyText XML attribute which is used as default text if there are no items in the adapter.

To enable the custom emptyText attribute, it must be declared as a styleable.

Next, let’s create a custom two-way data binding adapter to enable updating the selected item automatically. It will update the selected item according to the value from given LiveData, as well as updating the LiveData when the user selects another item. Implementing a two-way data binding adapter requires three methods:

  • binding adapter method which updates the view
  • inverse binding adapter method which retrieves the current value from the view
  • binding adapter method which listens to the changes in the view and notifies the given InverseBindingListener so that the data binding knows a change occurred and it should call the inverse binding adapter to retrieve the current value

Material Spinner Adapter

Second, we need a custom adapter which gets automatically notified by MaterialSpinner about selection changes and calls notifyDataSetChanged(). It also implements some boilerplate methods, namely getFilter() because AutocompleteTextView requires adapters to implement Filterable, even if we don’t want any filtering. Unfortunately, extending classes have to take care of the viewholder pattern manually (shown in an example below).

Example

Now all our components are ready. Let’s have a look at an example how to use them.

First, we extend the MaterialSpinnerAdapter and implement the getView() method. While the viewholder pattern is already built-in in RecyclerView, we have to use the old way here by using tags. This might be new for people who started Android development recently and have no experience with ListViews. Using viewholder pattern is necessary for reusing Views and avoiding inflating new Views which impacts performance.

The adapter inflates the binding object if it doesn’t exist yet (convertView is null) and sets it to the View’s tag so that it can be accessed later. Otherwise it just retrieves the object from the tag. Finally, it updates the binding object with the given position’s item and information whether the item is selected. The binding object is automatically generated from an XML layout which will be shown below.

We also need to create a custom binding adapter which will automatically set the ExampleSpinnerArrayAdapter with given list of items to the MaterialSpinner.

Spinner item layout utilizes the data binding way to update UI. In this example we simply update the text and enabled state according to the item. If the item is selected a mark icon is shown next to it (isNotGone is a custom binding adapter which simply makes the item visible if true, otherwise gone).

Finally, we can create the layout which shows the Spinner. As you can see, the MaterialSpinner must be wrapped by TextInputLayout which sets attributes such as hint, icons or box mode. Feel free to set these attributes as necessary.

For passing data we use the custom items binding adapter, as well as selectedPosition two-way data binding adapter so that both data and UI gets updated both ways. Note that the equals sign must follow behind the at sign to enable the two-way binding, i. e. “@={viewModel.selectedItemPosition}”. The selectedItemPosition’s type should be MutableLiveData<Int>.

Conclusion

Implementing a good looking Spinner in Android is more difficult than it should be. Hopefully after reading this article it is more understandable and the implementation will be easier once the reusable components are adopted.

--

--