Android Pagination: Error Handling

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

This is the third post in the Android Pagination article series.

In the previous article, (Android Pagination: Using APIs with Retrofit and Gson), 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.

This article has been updated for AndroidX!

Pagination Series Overview

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

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

Those who’ve followed my Pagination post series, know that 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 a 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>
    <androidx.recyclerview.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 the 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. But 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 retrying 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 an appropriate error message
  4. allowed users to retry the 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 android:layout_width="match_parent"
             android:layout_height="wrap_content">

    <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:background="?attr/selectableItemBackground"
        android:clickable="true"
        android:orientation="horizontal"
        android:paddingBottom="@dimen/activity_margin"
        android:paddingTop="@dimen/activity_margin"
        android:visibility="gone">

        <ImageButton
            android:id="@+id/loadmore_retry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="@dimen/activity_margin_content"
            android:layout_marginStart="@dimen/activity_margin_content"
            android:background="@drawable/rety_selector"
            android:padding="@dimen/activity_margin_half"
            android:src="@drawable/ic_refresh_black_24dp"
            android:tint="@color/placeholder_grey"
            android:tintMode="src_in"
            tools:targetApi="lollipop"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginEnd="@dimen/activity_margin_content"
            android:layout_marginLeft="@dimen/activity_margin"
            android:layout_marginRight="@dimen/activity_margin_content"
            android:layout_marginStart="@dimen/activity_margin"
            android:gravity="center_vertical"
            android:orientation="vertical">

            <TextView
                android:id="@+id/loadmore_errortxt"
                style="@style/Base.TextAppearance.AppCompat.Body1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <TextView
                style="@style/Base.TextAppearance.AppCompat.Caption"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                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 the PaginationAdapterCallback interface. Then, go to the loadNextPage() method and make the following changes:

private void loadNextPage() {
 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 responsible 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.

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

I hope this post helped make your paginated apps error-prepared.

Read nextPart 4: Handling multiple View Types with Pagination.

As always, I’d like to hear what you think. Tell me if I covered all the use cases. Did I miss out on 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

Product Designer who occasionally writes code.

You may also like...

9 Responses

  1. vaishnavi pagore says:

    Hello Sir, Iam getting an Error after refreshing the data 5-4 times its throwing and ARRAYINDEXOUTOFBOUNDSEXCEPTION …. not able to understand how to solve it please help me .

    java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
    at java.util.ArrayList.get(ArrayList.java:439)
    at com.probuz.in.DisplayGravAdapter.DisplayGravAdapter.getItem(DisplayGravAdapter.java:202)
    at com.probuz.in.DisplayGravAdapter.DisplayGravAdapter.removeLoadingFooter(DisplayGravAdapter.java:192)
    at com.probuz.in.SubFragments.DisplayGrevence$4.onResponse(DisplayGrevence.java:271)
    at retrofit2.ExecutorCallAdapterFactory$ExecutorCallbackCall$1$1.run(ExecutorCallAdapterFactory.java:71)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:211)
    at android.app.ActivityThread.main(ActivityThread.java:7234)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:503)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:887)

  2. Drake Colin says:

    waw great works

  3. Deeps Bhandari says:

    one of the best example

  4. Karlsanada13 says:

    I really love your tutorials. I am learning a lot.:3

  5. Ilya says:

    Thanks for tutorial, it`s really cool !
    But I have a null pointer exception with your code. Can you help me& Thanks in advance.

    E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.lukienko.movieapp, PID: 25633
    java.lang.NullPointerException: Attempt to invoke virtual method ‘java.util.List com.lukienko.movieapp.models.TopRatedMovies.getResults()’ on a null object reference
    at com.lukienko.movieapp.MainActivity.fetchResults(MainActivity.java:151)
    at com.lukienko.movieapp.MainActivity.access$700(MainActivity.java:32)
    at com.lukienko.movieapp.MainActivity$3.onResponse(MainActivity.java:129)
    at retrofit2.ExecutorCallAdapterFactory$ExecutorCallbackCall$1$1.run(ExecutorCallAdapterFactory.java:68)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:7325)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)

  6. Andy Smith says:

    Thanks for this awesome toturials. Please post Github code for this tuttorial

Leave a Reply

Your email address will not be published. Required fields are marked *