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
- Android Pagination Tutorial: Getting Started
- Using APIs with Retrofit and Gson
- Error Handling
- Using Multiple RecyclerView Types
- 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.
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>
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:
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:
- Hide the
ProgressBar
(loading indicator) - Display error layout
- 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:
- modified our activity_main.xml layout to include an error view
- displayed error view whenever the one-off API call failed
- used
onFailure()
to display an appropriate error message - 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:
- Hide the footer
ProgressBar
and show an error (in the footer) instead - Tell the user what went wrong
- 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>
Notice that our design now does two things. It:
- allows a user to retry
- 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:
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 next – Part 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!
Product Designer who occasionally writes code.
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)
waw great works
one of the best example
I really love your tutorials. I am learning a lot.:3
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)
Hi,
This error is most likely because of an issue with the API call’s response. Are you getting a response from the API?
Thanks for this awesome toturials. Please post Github code for this tuttorial
Hi Andy,
You’re welcome. Post is now updated with GitHub link (see end of post). Sorry about that!
Thanks so much!