Android Pagination: Error Handling

Endless or Infinite scrolling is called pagination. You do this in Android using RecyclerView. However there are few critical error scenarios to handle.


This is the third post in a series of Android Pagination articles.

In the previous, Android Pagination: Using APIs with Retrofit and Gson article, we fetched real API data in Pagination. We used themoviedb.org for our data source. Additionally Retrofit was used for networking, Glide for image loading and Gson for JSON parsing.

However, when networking is involved, one simply cannot ignore the various errors that occur. A good app intelligently handles all errors. This article aimed towards that.

Pagination Series Overview

  1. Android Pagination Tutorial: Getting Started
  2. Using APIs with Retrofit and Gson
  3. Error Handling
  4. Using Multiple RecyclerView Types (coming soon)
  5. Adding Swipe-to-Refresh Support (coming soon)

Unlike one-off API calls, which involves one request to display data on-screen, Pagination is different.

Those who’ve followed my Pagination post series, know the first page call happens separately. This means, error handling for page 1 will be different than that for page 2 and beyond.

Pagination involves 2 crucial error handling scenarios. Let’s deal with both.

Pagination Error Handling for Page 1

For pagination, page 1 is our one-off API call.

Now if the API call fails here, the screen will be empty with nothing to display. So unless you have cached data as backup, you have nothing to show.

One-off loading content

So what can we do? Use the entire screen to tell people what went wrong.

Ideally, stick to a crisp one or two liner about the error. Nothing too technical. Also, don’t forget to include a ‘Call-to-Action’ (CTA) button.

Mostly, the CTA would and should be a retry button.

Here’s how I’ve modified my existing screen to accommodate an error layout:

activity_main.xml layout skeleton

<FrameLayout>
    <android.support.v7.widget.RecyclerView />
    <ProgressBar/>
    <include layout="@layout/error_layout"/>
</FrameLayout>

error_layout.xml

<merge
       android:layout_width="match_parent"
       android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/error_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="vertical"
        android:visibility="gone"
        tools:visibility="visible">

        <!--Displays a generic error message-->
        <TextView
            style="@style/TextAppearance.AppCompat.Subhead"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/error_msg"/>

  <!--Displays a reason for the error—>
        <TextView
            android:id="@+id/error_txt_cause"
            style="@style/TextAppearance.AppCompat.Caption"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="@dimen/activity_margin_quarter"
            tools:text="The server took too long to respond."/>

        <!—CTA— prompting user to retry failed request>
        <Button
            android:id="@+id/error_btn_retry"
            style="@style/Widget.AppCompat.Button.Borderless"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/activity_margin_content"
            android:text="@string/btn_retry"
            android:textColor="@color/colorPrimary"/>

    </LinearLayout>
</merge>

One-off API call error layout

Use humor to reduce the  seriousness of error

While this is the bare minimum, use the available screen estate effectively. Use this opportunity to display an image associated with the error. Even better if you can use a short GIF to go with it.

Some examples from material.uplabs.com:

Google Playbook – Error Animation by Ben Breckler

 

Error Screen by Amit Nanda

Remember, adding a bit of humor goes a long way. However, this may offend a few so use it sparingly.

Handling Failure

Once the call fails, we must do three things:

  1. Hide the ProgressBar (loading indicator)
  2. Display error layout
  3. Show the appropriate error message

We can handle all of this by writing a convenient method showErrorView(Throwable t) called in onFailure() of our API call.

callTopRatedMoviesApi().enqueue(new Callback<TopRatedMovies>() {
           @Override
           public void onResponse(Call<TopRatedMovies> call, Response<TopRatedMovies> response) {
               hideErrorView();
               // TODO: handle data
           }

           @Override
           public void onFailure(Call<TopRatedMovies> call, Throwable t) {
               t.printStackTrace();
               showErrorView(t);
           }
       });

 private void showErrorView(Throwable throwable) {
       if (errorLayout.getVisibility() == View.GONE) {
           errorLayout.setVisibility(View.VISIBLE);
           progressBar.setVisibility(View.GONE);

           // display appropriate error message
           // Handling 3 generic fail cases.
           if (!isNetworkConnected()) {
               txtError.setText(R.string.error_msg_no_internet);
           } else {
               if (throwable instanceof TimeoutException) {
                   txtError.setText(R.string.error_msg_timeout);
               } else {
                   txtError.setText(R.string.error_msg_unknown);
               }
           }
       }
   }

showErrorView() takes a Throwable parameter, that can be used to know what the error is.

NOTE
Don’t forget to hide the error view! A good place to do this is BEFORE executing your API call.

Notice our error layout has a retry button. This is our CTA which effectively allows to retry the failed request. So hopefully our screen will have something to show now.

Next, let’s add a click listener.

 btnRetry.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view { 
          loadFirstPage(); 
      }
});

The button allows users to retry the first request, if it failed.

Let’s run through a quick recap of what we just did. We:

  1. modified our activity_main.xml layout to include an error view
  2. displayed error view whenever the one-off API call failed
  3. used onFailure() to display appropriate error message
  4. allowed users to retry request after failure, via a CTA (Retry Button)

Pagination Error Handling for Page 2 and above

As mentioned earlier, errors for page 2 and above will be handled differently. Initially, when expecting a page to load, a ProgressBar (loading indicator) is display at the footer.

If the API successfully returns a response, then we remove our ProgressBar and append the new data. However, for some reason if that fails, we need to handle it.

Similar to page 1, we need to do two things:

  1. Hide the footer ProgressBar and show an error (in the footer) instead
  2. Tell the user what went wrong
  3. Allow them to retry

While the steps to perform remain the same, the key difference lies in executing this and displaying it to the user.

Open item_progress.xml. We need to add an error layout to this.

<FrameLayout 
       xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             tools:background="#fafafa">

    <ProgressBar
        android:id="@+id/loadmore_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>

    <LinearLayout
        android:id="@+id/loadmore_errorlayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:paddingBottom="@dimen/activity_margin"
        android:paddingTop="@dimen/activity_margin"
        android:visibility="gone"
        tools:visibility="visible">

        <ImageButton
            android:id="@+id/loadmore_retry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:background="@drawable/rety_selector"
            android:src="@drawable/ic_refresh_black_24dp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <TextView
                android:id="@+id/loadmore_errortxt"
                tools:text="What went wrong"/>

            <TextView
                android:text="@string/tap_to_reload"/>

        </LinearLayout>
    </LinearLayout>
</FrameLayout>

(Footer) Progress layout with retry UI

Notice that our design now does two things. It:

  1. allows a user to retry
  2. tells them what went wrong

Listening to Retry button clicks

Go to PaginationAdapter.java. Let’s add the click action for the retry button.

protected class LoadingVH extends RecyclerView.ViewHolder implements View.OnClickListener {
     private ProgressBar mProgressBar;
     private ImageButton mRetryBtn;
     private TextView mErrorTxt;
     private LinearLayout mErrorLayout;

     public LoadingVH(View itemView) {
         super(itemView);

   …
         mErrorTxt = (TextView) itemView.findViewById(R.id.loadmore_errortxt);
         mErrorLayout = (LinearLayout) itemView.findViewById(R.id.loadmore_errorlayout);

         mRetryBtn.setOnClickListener(this);
         mErrorLayout.setOnClickListener(this);
     }

     @Override
     public void onClick(View view) {
         switch (view.getId()) {
             case R.id.loadmore_retry:
             case R.id.loadmore_errorlayout:
                 showRetry(false, null);
                 mCallback.retryPageLoad();
                 break;
         }
     }
 }

Notice that mCallback.retryPageLoad() is a listener which MainActivity will implement.

public interface PaginationAdapterCallback {
    void retryPageLoad();
}

Head back to MainActivity.java and implement PaginationAdapterCallback interface. Then, go to the loadNextPage() method and make the following changes:

private void loadNextPage() {
       Log.d(TAG, "loadNextPage: " + currentPage);

       callTopRatedMoviesApi().enqueue(new Callback<TopRatedMovies>() {
           @Override
           public void onResponse(Call<TopRatedMovies> call, Response<TopRatedMovies> response) {
               adapter.removeLoadingFooter();
               isLoading = false;
         …
           }

           @Override
           public void onFailure(Call<TopRatedMovies> call, Throwable t) {
   adapter.showRetry(true, fetchErrorMessage(t));
           }
       });
   }

Add the showRetry() method in PaginationAdapter.java.

public void showRetry(boolean show, @Nullable String errorMsg) {
      retryPageLoad = show;
      notifyItemChanged(movieResults.size() - 1);

      if (errorMsg != null) 
    	this.errorMsg = errorMsg;
  }

adapter.showRetry() is responsibly for changing the footer ProgressBar into a retry button. It additionally displays an error message saying what went wrong.

 

Displaying an error message matters

Typically, apps tend to show a retry button on failure. They miss out on telling us what exactly went wrong. The reason for failure could be anything. Does the user have a network issue? Or did the server fail to respond?

Most importantly, the error message tells us on whose side the issue is.

Here is how Instagram and DailyHunt (NewsHunt) handle pagination errors:

Pagination error handling in Instagram

DailyHunt pagination error handling

Take a good look at both these examples. Which one do you think did a better job of telling you what went wrong? That’s right, Dailyhunt. The app clearly tells us what the issue is. Also, it tells us what to do, to rectify it. Finally, the error footer is clickable, allowing us to retry that failed request.

While Instagram certainly does have a clean design, simply showing a retry option doesn’t really tell us the problem.

Finally, with everything done, let’s go ahead and run our app.


Final Results

We’ve now successfully handled both use cases. Page 1 is a separate call compared to the rest of the pages.

When page 1 fails, we don’t have any content to show the user. This leaves the entire screen blank. We handled this by smartly using the available screen estate. An appropriate error message was displayed. Additionally, we allowed users to retry the failed request.

We then followed a similar approach to handle page 2 and beyond. In addition to display a retry button in the footer, we displayed the reason for failure as well. Finally, we inferred from 2 famous apps about how they handled a similar scenario.

SOURCE CODE:

View Project on GitHub

 

Some key takeaways:

  • Pagination primarily has 2 error cases to be handled
  • Errors can be unavoidable, but we can take steps to handle them
  • Tell users what exactly went wrong, but don’t get too technical
  • Be forgiving with errors (consider using humor) and allow users to retry requests

Hope this post helped make your paginated apps error-prepared. As always, I’d like to hear what you think. Tell me if I covered all use cases. Did I miss out anything? Drop ’em in the comments below.

I’m happy you took the time to read this. All I need for you is to help me spread the word!

Suleiman

Android developer and tech enthusiast. Love creating apps with good UI UX design. I also like to sketch and listen to Chillstep.

You may also like...