Stop flashing your Android users
In web development, there’s a well-known problem called the “flash of unstyled content” — as a page loads, its content is initially rendered unstyled by the browser, but a split-second later the appearance may change drastically once the CSS stylesheets have loaded, parsed and been applied.
In Android apps there can be a different, but similarly distracting problem where the user is briefly flashed with one screen layout before another layout appears.
For example, showing a spinner while some data is fetched asynchronously from SQLite, disk or the network and then swapping the “loading” UI layout with the real layout once the data arrives. While it is good practice to perform potentially long operations on a background thread, showing the user appropriate UI while it happens, often the data loads very quickly — e.g. because your SQLite database has already been opened, or the data was in the local HTTP cache. This would cause your user to see the “please wait” UI flash up on screen for only a very short period of time.
While this isn’t the end of the world, it’s an irritation I like to avoid.
Solution
Having seen this effect in a couple of apps I’ve worked on, I’ve used a pretty straightforward solution.
In cases where the loading operation does take a long time, we want to show the spinner, otherwise not. Since we can’t predict IO or network latencies to know whether we should show the spinner, we simply delay showing it for a short period of time — long enough that the user doesn’t get flashed, but short enough that the user doesn’t perceive the UI to freeze and then the spinner appears and then the content appears.
I’ve found (subjectively) that 150 milliseconds is an ample delay. Most local operations can easily complete within this time, e.g. reading from a database, opening and decoding an image, or returning data from disk cache. A network request can also complete in this time (though of course this depends on a number of factors). But the delay isn’t so long that the user thinks the UI isn’t responding to their request.
Implementation
The Handler
class not only makes it simple to queue events to be executed in the future, but also to cancel queued events. Just before starting the asynchronous data fetch, we call sendEmptyMessageDelayed()
on a handler, requesting that the spinner should be shown 150 milliseconds from now.
/** Time to wait for the DB before showing {@link #mLoadingView}. */
private static final int DB_LOADING_GRACE_MS = 150;
public void onResume() {
super.onResume();
// Schedule the spinner to be shown, after a short delay
mHandler.sendEmptyMessageDelayed(MSG_SHOW, DB_LOADING_GRACE_MS);
// Trigger fetch from database
getLoaderManager().initLoader(0, null, this);
}
Once the asynchronous fetch has ended or was cancelled, we tell the handler to hide the spinner and, in this case, the regular layout is displayed by way of populating a list adapter:
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mHandler.sendEmptyMessage(MSG_HIDE);
mAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mHandler.sendEmptyMessage(MSG_HIDE);
mAdapter.swapCursor(null);
}
Looking at the Handler
implementation itself, when MSG_HIDE
is handled we simultaneously cancel any queued requests to show the spinner before making sure that the spinner is hidden, in case it was already shown:
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_SHOW) {
// Show the spinner
mLoadingView.setVisibility(View.VISIBLE);
} else {
// If loading took less than 150ms,
// ensure that the spinner won't be shown later
removeMessages(MSG_SHOW);
// If loading took more than 150ms,
// hide the currently-visible spinner
mLoadingView.setVisibility(View.GONE);
}
}
};
When data loads quickly…
If fetching the data is pretty fast from the user’s standpoint, say within 90ms, the flow would be:
- A
MSG_SHOW
event is scheduled - Data is requested in the background via the loader
- The loader returns the data
- A
MSG_HIDE
event is executed immediately - The scheduled
MSG_SHOW
event is removed from the queue - The loaded data is shown in the UI
So the user is not briefly flashed with a spinner; only the result is shown, despite the data not being available instantly to the UI.
When data loads slowly…
If the data load takes longer — 1600ms in this example — the events would look like:
Time | Event |
---|---|
0 | A MSG_SHOW event is scheduled |
1 | Data is requested in the background via the loader |
150 | The scheduled MSG_SHOW event executes; spinner is shown |
… | |
1601 | The loader returns the data |
1602 | A MSG_HIDE event is executed immediately |
1602 | The spinner is hidden |
1603 | The loaded data is shown in the UI |
So, the users sees the spinner because loading the data is taking a while.
Conclusion
A couple of simple code snippets — using only APIs which have been in the Android SDK since the first release — can help add a bit more polish to your app for the majority of cases where you’re loading data for the user.
While of course users may still be perceptibly flashed if loading data takes something like 300ms, this technique eliminates unnecessary flashing for many requests.
Not only does it work on every Android version, threading isn’t a worry either since all UI actions happen in the Handler
which is declared, and therefore runs, on the UI thread.