Android Pagingライブラリのエラーハンドリング方法をサンプルから読み解く

Android Architecture Componentsの一つに、RecyclerViewを使ったページネーションを行うためのライブラリである[Paging](https://developer.android.com/topic/libraries/architecture/paging/)があります。
Pagingライブラリでは、エラーハンドリングに関する機能が何も提供されていないため、これを自分で実装する必要があります。

エラーハンドリングの実装例は[公式サンプル](https://github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample)を参照せよとのことなので、これを読み解いていきたいと思います。
ここでは`NETWORK ONLY (BY PAGE)`を使用します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/20e422c4-d823-cb3f-0ef5-1aa7633eb2fc.png)

サンプルは頻繁に更新されているので、念の為今回使用したコミットハッシュ値を載せておきます。
[4f64b33eda70c22669324b038e0ef451208960a7](https://github.com/googlesamples/android-architecture-components/commit/4f64b33eda70c22669324b038e0ef451208960a7)

## エラー時の画面
画面を下方向にスクロールしていくと、追加のページを取得しようとします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/b02e3731-20e3-0c04-1273-babee96db682.png)


このとき、通信のタイムアウトなどのエラーが発生すると追加ページの読み込みを中断し、エラーメッセージとリトライボタンが表示されます。
このエラーメッセージとリトライボタンはRecyclerViewの最終行に表示されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/17a5c47a-dfef-f062-7a9b-1e1bf9d0decc.png)


リトライボタンをタップすると追加ページの読み込みを再開します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/e84b87e0-652e-498e-390d-b3f5d53172bc.png)

## コードを読む
### RecyclerViewの設定
`RedditActivity`の以下の部分がRecyclerViewのアダプターの設定をしている箇所です。

`model.posts`は取得したページが流れてくるLiveDataオブジェクトで、新しいページが到着したらそれをアダプターに渡します**（1）**。

`model.networkState`はネットワーク通信の状態を表す`NetworkState`オブジェクトが流れてくるLiveDataオブジェクトで、新しい`NetworkState`が到着したらそれをアダプターに渡します**（2）**。
`adapter.setNetworkState()`は、`NetworkState`が`LOADING`の場合にはProgressBarを、`error`の場合にはエラーメッセージとリトライボタンを表示する行を追加します。
`LOADED`になったらその行を削除します。

```kotlin:RedditActivity.kt
private fun initAdapter() {
    val glide = GlideApp.with(this)
    val adapter = PostsAdapter(glide) {
        model.retry()
    }
    list.adapter = adapter

    model.posts.observe(this, Observer<PagedList<RedditPost>> {
        adapter.submitList(it)  // (1)
    })

    model.networkState.observe(this, Observer {
        adapter.setNetworkState(it)  // (2)
    })
}
```

### ページネーションの実装
ページネーション実装の本体は`PageKeyedSubredditDataSource`クラスです。
ここでは追加ページ取得時のエラーハンドリングに絞って見ていきます。

追加ページの取得を行うメソッドは`loadAfter()`です。

最初に`NetworkState.LOADING`を通知して、画面にProgressBarを表示させるようにしています**（1）**。

続いて、`redditApi.getTopAfter()`でAPIから追加ページを取得します。
ここではエラー時のコールバックメソッドを見てみます**（2）**。

`retry`は`PageKeyedSubredditDataSource`クラスの関数型のプロパティです。
リトライボタンがタップされたときに実行されるクロージャをここでセットしています。
そして、ページ取得に失敗したことを`NetworkState.error`で通知しています。
この`NetworkState.error`は`SubRedditViewModel`を通じて`RedditActivity`に通知されます。

```kotlin:PageKeyedSubredditDataSource.kt
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, RedditPost>) {
    networkState.postValue(NetworkState.LOADING)  // (1)

    redditApi.getTopAfter(subreddit = subredditName,
            after = params.key,
            limit = params.requestedLoadSize).enqueue(
            object: retrofit2.Callback<RedditApi.ListingResponse> {
                // (2)
                override fun onFailure(call: Call<RedditApi.ListingResponse>, t: Throwable) {
                    retry = {
                        loadAfter(params, callback)
                    }
                    networkState.postValue(NetworkState.error(t.message ?: "unknown err"))
                }
```

### リトライボタンのクリックリスナー設定
最後に、リトライボタンがタップされたときにリトライを実行するための設定を見ていきます。

`loadAfter()`内でセットしたクロージャは、`SubRedditViewModel`を通じて`RedditActivity`が参照できるようになっています。
RecyclerViewの設定のところで、`PostsAdapter`をインスタンス化するときにこのクロージャが渡されています**（1）**。

```kotlin:RedditActivity.kt
private fun initAdapter() {
    val glide = GlideApp.with(this)
    // (1)
    val adapter = PostsAdapter(glide) {
        model.retry()
    }
    list.adapter = adapter

    model.posts.observe(this, Observer<PagedList<RedditPost>> {
        adapter.submitList(it)
    })

    model.networkState.observe(this, Observer {
        adapter.setNetworkState(it)
    })
}
```

さらに`PostsAdapter`を見ていくと、リトライボタンを表示するViewHolder(`NetworkStateItemViewHolder`)をインスタンス化するときにこのクロージャを渡しています**（1）**。

```kotlin:PostsAdapter.kt
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when (viewType) {
        R.layout.reddit_post_item -> RedditPostViewHolder.create(parent, glide)
        // (1)
        R.layout.network_state_item -> NetworkStateItemViewHolder.create(parent, retryCallback)
        else -> throw IllegalArgumentException("unknown view type $viewType")
    }
}
```

最後に`NetworkStateItemViewHolder`を見てみると、コンストラクタでリトライボタンにクリックリスナーがセットされています。
これでリトライボタンをタップしたときに`loadAfter()`が実行されるようになるというわけです。

```kotlin:NetworkStateItemViewHolder.kt
init {
    retry.setOnClickListener {
        retryCallback()
    }
}
```

## まとめ
今回全てのコードの説明はしていませんが、サンプルを読む際に本記事がヒントになれば幸いです。

リトライボタンを使ってユーザにリトライさせるというのはエラーハンドリングの一例であり、公式ガイドではエラー発生時は自動でリトライするのがベストだとも言っています。
自動リトライをさせるのであれば、エクスポネンシャルバックオフを取り入れるなどの工夫が必要かなと思っています。