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:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<!-- assume these text views should turn read on error -->
<TextView
android:id="@+id/label1"
style="@style/MyLabelStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Label1" />
<TextView
android:id="@+id/label2"
style="@style/MyLabelStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Label2" />
</LinearLayout>
view raw layout.xml hosted with ❤ by GitHub

And a naive way to switch the label colors when there’s an error:

public class Switcher extends Fragment {
private ColorStateList mLabel1OriginalTextColor;
private ColorStateList mLabel2OriginalTextColor;
private TextView mLabel1;
private TextView mLabel2;
public void onViewCreated(Bundle savedInstance) {
mLabel1OriginalTextColor = mLabel1.getTextColors();
mLabel2OriginalTextColor = mLabel2.getTextColors();
}
public void setError(boolean error) {
if (error) {
// we should probably get this from a resource!
mLabel1.setTextColor(Color.RED);
mLabel2.setTextColor(Color.RED);
} else {
mLabel1.setTextColor(mLabel1OriginalTextColor);
mLabel2.setTextColor(mLabel2OriginalTextColor);
}
}
}
view raw Switcher.java hosted with ❤ by GitHub

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.

Using selectors

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:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.example.app">
<item app:state_error="true" color="@color/color_error"/>
<item android:state_pressed="true" color="@color/color_pressed"/>
<item android:color="@color/default_color"/>
</selector>
view raw error_color.xml hosted with ❤ by GitHub

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:

<resources>
<declare-styleable name="ErrorState">
<attr name="state_error" format="boolean"/>
</declare-styleable>
</resources>
view raw attrs.xml hosted with ❤ by GitHub

Then we should actually apply that state. We can do that in two ways:

  1. Extend TextView
  2. Extend the LinearLayout containing the TextViews

In this case we’ll extend the LinearLayout like this:

public class ErrorStateLinearLayout extends LinearLayout {
private boolean mError = false;
private static final int[] ERROR_STATE = new int[] { R.attr.state_error };
public ErrorStateLinearLayout(Context context) {
super(context);
}
public ErrorStateLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
int[] state = super.onCreateDrawableState(extraSpace + 1);
if (mError) {
mergeDrawableStates(state, ERROR_STATE);
}
return state;
}
public void setError(boolean error) {
this.mError = error;
refreshDrawableState();
}
}

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:

<?xml version="1.0" encoding="utf-8"?>
<com.example.app.ErrorStateLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<TextView
android:id="@+id/label1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:text="Label1"
android:textColor="@color/error_color" />
<TextView
android:id="@+id/label2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:text="Label2"
android:textColor="@color/error_color" />
</com.example.app.ErrorStateLinearLayout>

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:

public class Switcher extends Fragment {
private ErrorStateLinearlayout mContainer;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mContainer = (ErrorStateLinearlayout) inflater.inflate(R.layout.error_layout, container, false);
return mContainer;
}
public void setError(boolean error) {
mContainer.setError(error);
}
}
view raw Switcher.java hosted with ❤ by GitHub

Much cleaner code, less bugs, yay for us!

Conclusion

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!

Comment on this post on Google+