Short rant: Google has a dream. A terrible, monomaniacal dream. Material Design portends the worldwide unification of both the aesthetic and language of design . Never-mind that the design itself is rooted in the absolute human travesty that is flat design. What about the hubris of imposing a single design upon the entire world? Witness the end of creative endeavor. Witness the end of individualism. Witness the dumbing down of humanity. Witness Donald Trump.
Material Design Components for Android, (MDCA) in version
1.2.1 at the time of this writing, has some serious head-scratchers that are guaranteed to waste copious amounts of your precious time and sanity. This article will cover one of the most insidious:
You read this correctly. Right now, you’re asking yourself:
How the HELL could Material Design Components for Android NOT include a Spinner component? Every Android app needs a Spinner! How am I expected to implement a single-select dropdown list without a Spinner?
Want to read this story later? Save it in Journal.
Actually, there is a Material Design
Spinner. It’s just not called a
Spinner. And it’s not implemented like a
Spinner, either. The remainder of this article will correct your foolish understanding of reality while delivering yet another sad truth regarding your Android development experience: You now have lots of extra work to do. Yay! Let’s get started!
Before we get started, it’s important to note that although this article will help you generate a proper Material Design Spinner, the spinner itself is arguably somewhat worthless due to a critical UI failure of the menu, which will regularly pop up instead of dropping down. Spinner menus should always drop down. But instead of automatically expanding the current UI downward to make room for the menu to drop down, if your spinner is located anywhere below the half the screen height, the menu will pop up instead. I consider this to be a serious UI anti-pattern. But FAR worse, the menu will regularly appear over the top of the spinner itself, completely blocking the spinner. When this happens, you will be unable to see the spinner’s hint OR its currently selected item. This is a much more egregious anti-pattern.
In my own Android apps, I no longer use spinners, unless I can guarantee that the spinner will be shown at or near the top of the current screen. For all other cases, I’m forced to render a text field that when clicked, renders a
DialogFragment containing a
RecyclerView of the spinner’s items. When an item is selected, I close the dialog and set the text field’s text to the selected item. It’s a serious pain, but there you have it. If these limitations are OK with you, then read on to discover how to properly implement a material spinner.
Learn this: The Material Design
Spinner component you are looking for has been silently replaced with Exposed Dropdown Menu. Exposed Dropdown Menu is a term coined by the Material Design team to describe a dropdown menu with the selected item displayed above the dropdown.
The problem is, Android already has both a component and a term that describes this behavior: It’s called a
In their attempt to unify the entire universe of design, the Material Design team has decided that the name Spinner is not universally applicable as a Material Design concept in every context, and thus will not suffice in any context, including in Android itself! They expect you to attend Material Design University to absorb all of their universal design language before you can leverage any of it. How very Android.
After reading mountains of literature about things you likely care nothing about, the Material Design team assumes you’ll automagically realize that you never really wanted a
Spinner, because there’s no such thing. What you really want is an Exposed Dropdown Menu. And armed with your superior understanding, you’ll also automagically know to search under Menu in their Android Components documentation, instead of searching for the
Spinner you erroneously thought you wanted. Genius.
The awfulness of this can hardly be quantified.
To instantiate a Material Design
Spinner, ahem, Exposed Dropdown Menu, instead of the straightforward
Spinner XML you’re used to, you’re now expected to do this:
Not the end of the world, but far less than ideal, and clearly a hack.
Don’t forget a whole ‘nuther layout for each Spinner item (we’ll configure
Chief among the litany of problems with this hack is that with it, the Material Design team has casually forced a complete conceptual parity mismatch upon the entire Android developer community. You’re now expected to think in terms of a
Spinner, but code in terms of a
TextInputLayout and an
For instance, how do you detect when the user selects a new
Spinner item? Intuitively, we expect to listen for selection changed or selected item changed. But there is no
onItemSelectedListener for Material Spinners, because a Material Spinner isn’t a
Spinner. Instead, we’re forced to add a text changed listener to the
AutocompleteTextView to get what we want. See the problem here? You think in terms of item selected. But you code in terms of text changed — just one example of the parity mismatch between a proper
Spinner and this horrifying hack of a solution.
The real fun begins with styling. Oh boy, we’re in for a special ride on this one. After only 900 hours of effort, I’ve managed to figure out what I consider to be the right way to style an
ExposedDropdownMenu . This includes properly styling the parent
TextInputLayout as well as its child
AutoCompleteTextView, including the popup menu that acts as the spinner’s dropdown.
This style is part of my
MyApp theme which is based on
Theme.MaterialComponents.DayNight , which defines core material styles and colors like
Notice the use of the
materialThemeOverlay. The purpose of a material theme overlay is to define a custom theme that can be applied to specific components. But strangely, you use a material theme overlay BOTH to style the current component AND to gain critical access to styling its children. In this case, we leverage an overlay to style not just the outer box around our spinner, but also the child
Almost forgot: In the style above, we set
@color/material_input_border is a state color list created in res/color/material_input_border.xml that handles the border color around the spinner’s box depending on the
focused and active states:
Next up, let’s populate our Material Design
Spinner (or, for our fellow Manchurian candidates, Exposed Dropdown Menu) with items:
Everything seems fine here. Same ridiculously insane amount of code to implement a simple control as everything else in Android, so what’s the problem?
The problem is that instead of instantiating and wrapping a
Spinner component, you’re now instantiating and wrapping an
AutoCompleteTextView. Both components feature the dropdown menu (popup menu in Android reversed logic) we’re really after, so where’s the problem?
First, you’re expected to enter text into an
AutoCompleteTextView, but you never type text into a
Spinner. So you’re forced to remember to set the
android:editable=”false” attribute in your XML or your
Spinner won’t behave correctly. But upon doing so, Android Studio will rightly inform you that the
editable attribute is deprecated. Great. You have to use a deprecated attribute to get modern behavior. Insane, but true.
Second, and FAR worse, the default behavior of
AutoCompleteTextView is to filter the dropdown menu to only those items that match the text you typed. Well, we’ve just disabled editing, so no one will be typing anything into the box (our
Spinner). So we should be fine, right?
Check out what happens when you programmatically select an item from the list.
If you forget the second
false parameter, the one that instructs the
AutoCompleteTextView to NOT to perform filtering, then whenever you set the selected item programmatically, all dropdown items except the selected item will disappear from the list!
Dear God, please help me.
Finally, let’s talk Data Binding. Because our Material
Spinner (I simply refuse to say Exposed Dropdown Menu) can only be populated with
Strings (because it’s actually just an
AutoCompleteTextView and not the
Spinner we expect), if the underlying property we’re binding to is anything other than a
String, we’re going to have to craft a whole new data binding converter, to convert to and from
String and the data type we need.
In my case, my DB stores the selected day of the week as an
dow_index. To get my
Spinner to work with two-way data binding, I had to first manually craft a new data binding converter:
Android requires data binding converter classes (e.g.
Converter) to be defined as static classes. Hence the likely unfamiliar Kotlin syntax
With my converter class defined, I then use my converter in my data binding expression like so:
Note the invocation/usage of the
Converter in the data binding expression. IMO, the syntax is crap and reveals a core problem with the Android SDK, namely that you are required to learn and remember “magical” things. In this case, in order to obtain an instance of our converter, we’re required to know and use the magical
INSTANCE property. Then, we’re supposed to magically know and remember that Android provides ‘
context’ property for passing the current
Context into our data binding conversion method. In this case, we store weekday names in values.xml and thus our conversion methods require
Context in order to obtain the strings. BTW, having to provide
Context just to obtain a string is one of the many things that makes the Android SDK the worst thing on Earth.
Suppose the selected day of the week index in your
ViewModel is currently set to
2 (Tuesday). You open your app and see your Material
AutoCompleteTextView, I mean Exposed Dropdown Menu) with Tuesday successfully selected and rejoice to the heavens.
Then you click on the
Spinner to reveal its dropdown menu and select a different day, and another part of your soul dies as you witness Tuesday is now the ONLY option in the menu. Welcome back to square one. Your
AutoCompleteTextView is doing its default thing and filtering the list of options to those that match the current text. But now the selected text is data bound and there’s nothing you can do about it!
Dude, just kill me now. Please?
Actually, there is something you can do about it. And you’re going to love it.
ArrayAdapter and override the
getFilter method to return a new instance of a subclassed
Filter that does nothing.
Oh, right. Of course. That’s what I meant to say. Because I’m a genius, who knows everything that ever was or will be.
Here’s our manually-coded-up replacement for
And finally, we replace our earlier usage of
ArrayAdapter with our new subclass that performs no filtering:
So, it took me the better portion of 652 days to implement a
Spinner that’s styled like a Material Design component — or conversely, a Material Design Exposed Dropdown Menu that functions like a
All this living Hell on Earth in part because Android is the Frankenstein’s Monster of SDKs and in part because the Material Design team has simply done away with the venerable
Spinner, despite the fact that it’s precisely what is needed.
Just witness the deluge of code we’ve had to arduously craft:
200+ lines of code in 2 languages in 7 files in 4 directories using 5 different APIs
All of the above to implement one of the most common UI components in the most popular mobile platform on Earth. I wish I was kidding. This would be comical if it weren’t nearly criminal.
Did I mention that the Android SDK is the worst thing on Earth?
I hope this helps. Best of luck 😉