The how and the why

As a good Android developer always does (or rather, has to do) in these cases, it’s time to understand what’s going on under the hood.

Revvin’ up your engine

Let’s look into the TextView code for setError(CharSequence), first:

public void setError(CharSequence error) {

if (error == null) {

setError(null, null);

} else {

Drawable dr = getContext()

.getDrawable(

com.android.internal.R.drawable.indicator_input_error);

dr.setBounds(0, 0, dr.getIntrinsicWidth(),

dr.getIntrinsicHeight());

setError(error, dr);

}

}

So far, so good. As you can see, the setError(CharSequence) overload basically just calls the setError(CharSequence, Drawable) passing the default icon, as we’d expect. This will be important later on, so keep it in mind.

What does setError(CharSequence, Drawable) do, then?

public void setError(CharSequence error, Drawable icon) {

createEditorIfNeeded();

mEditor.setError(error, icon);

notifyViewAccessibilityStateChangedIfNeeded(

AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);

}

The most important thing here is the call to the underlying Editor. That means TextView is just delegating the error state handling to it.

Let’s look into that, then!

Listen to her howlin’ roar

Here’s Editor#setError():

public void setError(CharSequence error, Drawable icon) {

// ...

if (mError == null) {

setErrorIcon(null);

// Hide popup...

} else {

setErrorIcon(icon);

if (mTextView.isFocused()) {

showError();

}

}

}

I’m not going into the setErrorIcon() code, it does what it says on the tin, but in a quite convoluted way that’s not really worth explaining.

The important thing you need to know is that that setErrorIcon() sets your error icon as the compound drawable on the right side of the TextView. If your brain is now in pain and screaming:

Ewoks suck, dude.

CIRCULAR DEPENDENCIES!!1!

then it’s definitely right. The TextView delegates work to the Editor, which in turn acts on the TextView using its internal APIs. OOP at its finest.

Metal under tension

Ok, we have an idea of how the error is set and handled by the TextView and its Editor. Now, the question is: where does it all go wrong?

A fair assumption would be that the whole issue is related to the save/restore of the instance data of the TextView. Let’s see what’s in the onSaveInstanceState() code:

@Override

public Parcelable onSaveInstanceState() {

Parcelable superState = super.onSaveInstanceState();

// Save state if we are forced to

boolean save = mFreezesText; // ... if (save) {

SavedState ss = new SavedState(superState);

// ...

ss.error = getError();

return ss;

}

return superState;

}

Well, yes, apparently the current error state is saved in the TextView’s SavedState. That’s good.

But there’s something fishy here. We are only saving one field. One would hope that this is actually saving a compound object that wraps both the icon and the message, but that would be far, far too easy. Looking up TextView’s SavedState#error declaration, we see this:

CharSequence error;

Ruh-roh.

Yes, the TextView is only saving the error message.

Beggin’ you to touch and go

What happens then in the onRestoreInstanceState()? You get a comment:

// XXX restore buffer type too, as well as lots of other stuff

Errrrrm… well, thanks. I suppose this includes restoring the error icon at some point, but that isn’t clearly the case yet.

But wait — it gets better! Here’s how the error message is restored:

if (ss.error != null) {

final CharSequence error = ss.error;

// Display the error later, after the first layout pass

post(new Runnable() {

public void run() {

setError(error);

}

});

}

I won’t even begin to explain the level of WTF-ery of this code. I’ll just leave it here, for you to admire.