Christopher Orr

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:

  1. A MSG_SHOW event is scheduled
  2. Data is requested in the background via the loader
  3. The loader returns the data
  4. A MSG_HIDE event is executed immediately
  5. The scheduled MSG_SHOW event is removed from the queue
  6. 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
0A MSG_SHOW event is scheduled
1Data is requested in the background via the loader
150The scheduled MSG_SHOW event executes; spinner is shown
1601The loader returns the data
1602A MSG_HIDE event is executed immediately
1602The spinner is hidden
1603The 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.