A while ago, I built a form based app with lots of input fields and all the things that come with that, like validation and error messages.
In one of the situations, if a section is invalid, the field labels should turn red. Easy right? Here’s the example layout:
And a naive way to switch the label colors when there’s an error:
But this is kinda ugly. We need to get “not-error” color from each component, store it, switch the color to red and if the error is fixed, restore the colors again. There’s too much code that deals with updating the view in here and it only gets worse if we add more labels.
You’ve probably worked with selectors before. In this particular case we’ll be using a ColorStateList selector to change the color of the TextView. First, here’s the selector:
This is just like any other ColorStateList xml. You place it in the res/color/ folder of your project. This selector has one thing extra though: the app namespace and the app:state_error state. In effect this selector says:
- If the state is error the color used is color_error as defined in color.xml
- If the state is pressed, for example if the TextView is clickable and you click on it, the color used is color_pressed
- By default the color is default_color
By know you probably see where I’m going…If we can change the state of the TextView to app:state_error then the color should be set by the Android framework to the color_error that is defined. But how do we do that?
First we have to actually define the new state in res/values/attrs.xml:
Then we should actually apply that state. We can do that in two ways:
- Extend TextView
- Extend the LinearLayout containing the TextViews
In this case we’ll extend the LinearLayout like this:
This code should be easy to follow. We add an extra property mError to keep track of the error state. At line 3 a constant is defined containing an array of states that are set when mError is true. Then it’s just a matter of overriding onCreateDrawableState() which takes an int called extraSpace. Since potentially we’d like to add one extra state if the mError flag is set, the code calls super.onCreateDrawableState() with extraSpace + 1. Then, depending on the mError flag the error state is added to the list of states.
The final layout using this new component looks like this:
Things to notice in this layout:
- In stead of LinearLayout, ErrorStateLinearLayout is used as the main container
- Each TextView has android:duplicateParentState=”true” meaning that the TextView will inherit any states from the parent view group.
- Each TextView has it’s textColor set to error_color which would be the color state selector
Now we can finally update the view when there’s an error like this:
Much cleaner code, less bugs, yay for us!
By leveraging core Android principles, in this case using selectors, we can simplify our code a lot. By moving things that are related to the view out to xml resources not only our code gets cleaner, but we could also use the resources system to customize the ui based on the device configuration.
PS: ViewGroup has android:addStatesFromChildren which is equally useful!