［Android］Epoxy と Paging を組み合わせたサンプルと解説

# はじめに

Epoxyと Paging を組み合わせてページ毎にデータを読み込むリストを作成してみたいと思います。
アーキテクチャは Google が推奨している MVVM を利用して、次の手順で作成を進めます。

| No | タイトル |
| -------- | ------- |
| Step 0  | 必要なライブラリをセットアップする |
| Step 1  | Retorift でページ毎のデータを取得する |
| Step 2  | PagedList でページ毎のデータを管理する |
| Step 3  | EpoxyRecyclerView で表示する EpoxyModel を作成する |
| Step 4  | EpoxyRecyclerView で表示を制御する EpoxyController を作成する |
| Step 5  | 作成したクラスの動作を確認する |

完成イメージは次のような感じになります。

<a href="https://gyazo.com/f447b0c34a9fde8a5bd7df8c6a461314"><img src="https://i.gyazo.com/f447b0c34a9fde8a5bd7df8c6a461314.gif" alt="Image from Gyazo" width="250"/></a>

# アプリの構成

- PagedList がページ毎のデータの管理をする。
- PagedList を生成するには DataSource・DataSource.Factory が必要になる。
- PagedList を EpoxyRecyclerView で表示するには PagedListEpoxyController が必要になる。
- ページごとのデータ管理は PageList が中心となって行う、  
  そのため PagedList の生成や表示に必要となる周辺のクラスを実装していく必要がある。    
  それらの作成するクラスをまとめると次の表や図のような感じになる。  

| 分類 | 名称 | 説明 |
| ------ | ------- | ------- |
| View | PagedListEpoxyController | EpoxyRecyclerView に PagedList を表示するための Controller クラスの実装 |
| ViewModel | LiveData\<PagedList\<Item>> | ページ毎のデータ管理を行うためのクラスの実装 |
| Model | PageKeyedDataSource | PagedListを生成するのに必要となるクラスの実装 |
| Model | Data Source Factory | PagedListを生成するのに必要となるクラスの実装|
| Model | Service Class | ページ毎のデータ取得するクラスの実装 |

[![Image from Gyazo](https://i.gyazo.com/ac7072e51c661f503ce5888bab56c8d2.jpg)](https://gyazo.com/ac7072e51c661f503ce5888bab56c8d2)

# Step 0 必要なライブラリをセットアップする

アプリケーションの作成に必要となる、Epoxy・Retrofit・Gson などのライブラリをインストールする。
これらのライブラリを使うには kapt や databinding、 Java1.8 を有効にする必要があるのでこちらも設定しておきます。

| ライブラリ | バージョン | 説明 |
| ------- | ------- | ------- |
| Epoxy | 3.11.0 | RecyclerView ライブラリ |
| Retrofit | 2.9.0 | HTTP クライアントライブラリ |
| Gson | 2.8.5 | JSONデータをJavaオブジェクトの相互変換ライブラリ|
| Paging | 2.1.2 | Paging ライブラリ |


```groovy
apply plugin: 'kotlin-kapt'

android {
    ︙
    ︙
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

    dataBinding {
        enabled = true
    }
    ︙
    ︙
}

dependencies {
    ︙
    ︙
    def epoxy_version = "3.11.0"
    implementation "com.airbnb.android:epoxy:$epoxy_version"
    implementation "com.airbnb.android:epoxy-databinding:${epoxy_version}"
    implementation "com.airbnb.android:epoxy-paging:${epoxy_version}"
    kapt "com.airbnb.android:epoxy-processor:$epoxy_version"

    def retrofit_version ="2.9.0"
    implementation "com.squareup.retrofit2:retrofit:${retrofit_version}"
    implementation "com.squareup.retrofit2:converter-gson:${retrofit_version}"

    def gson_version = "2.8.5"
    implementation "com.google.code.gson:gson:${gson_version}"
  
    def paging_version = "2.1.2"
    implementation "androidx.paging:paging-runtime:$paging_version" 
    ︙
    ︙
}
```

あとは Retrofit でネットワークを使って通信を行うので AndroidManifest.xml にて android.permission.INTERNET の権限を追加しておきます。

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.kaleidot725.sample">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ui.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```
# Step 1 Retorift でページ毎のデータを取得する

今回はページ単位の情報を取得する API として Qiita の記事一覧の取得 API  ([ GET /api/v2/items ](https://qiita.com/api/v2/docs#get-apiv2items)) を利用します。HTTP Client には Retrofit を利用します。なので次のクラスを作成して API を呼び出せるようにしていきます。

| 役割 | クラス名 | 役割 |
| ------- | ------- | -------- |
| Entity | Item | Qiitaの記事データを表すデータクラス |
| Service | QiitaService | Qiita API を利用するためのサービスクラス |
| Data Source | ItemDataSource | QiitaService を利用してページ毎のデータを取得するクラス |
| Data Source Factory | ItemDataSourceFactory | ItemDataSource を生成するクラス |

**Item**
Qiitaの記事データを表すデータクラスを宣言します。Item には Group や Tag や User といった情報も含まれるのであわせて定義していきます。API で利用するデータクラスは[JSON To Kotlin Class](https://plugins.jetbrains.com/plugin/9960-json-to-kotlin-class-jsontokotlinclass-)を利用すると JSON を入力するだけで自動生成してくれるので楽です。

```kotlin
data class Item(
    val body: String, val editing: Boolean, val comments_count: Int, val created_at: String,
    val group: Group, val id: String, val likes_count: Int, val page_views_count: Int, val `private`: Boolean,
    val reactions_count: Int, val rendered_body: String, val tags: List<Tag>, val title: String, val updated_at: String,
    val url: String, val user: User
)

data class Group(
    val created_at: String, val id: Int, val name: String,
    val `private`: Boolean, val updated_at: String, val url_name: String
)

data class Tag(val name: String, val versions: List<String>)

data class User(
    val description: String, val facebook_id: String, val followees_count: Int,
    val followers_count: Int, val github_login_name: String, val id: String, val items_count: Int,
    val linkedin_id: String, val location: String, val name: String, val organization: String, val permanent_id: Int,
    val profile_image_url: String, val team_only: Boolean, val twitter_screen_name: String, val website_url: String
)
```

**QiitaService**

Retrofit で Qiita API の [GET /api/v2/items ](https://qiita.com/api/v2/docs#get-apiv2items)を利用できるようにします。今回はページ毎に取得したいので page や per_page のクエリを追加しておきます。Retrofit の実装方法の詳細については[公式ドキュメント](https://square.github.io/retrofit/) を閲覧してください。

```kotlin
interface QiitaService {
    @GET("/api/v2/items")
    fun getItems(@Query("page")page: Int, @Query("per_page") perPage: Int): Call<List<Item>>
}
```

# Step2 PagedList でページ毎のデータを管理する

**PagedListの生成方法**

ページ毎に API からデータを取得する場合は PagedList を生成します。 PagedList の生成は LivePagedListBuilder で行いますが、そのときに DataSource と DataSource.Factory が必要になるので実装します。


**ItemDataSource**

DataSource で実際にどのようなデータをページ毎に生成するか決めます。今回は RecyclerView が表示されたら 1ページ目のデータ、末尾に到達したら次のページのデータを表示するように実装してみます。（データ取得には先程作成した、Qiitaの記事一覧取得処理を利用します。）

```kotlin
// API呼び出しをしているので、本来であればここで例外の対処を記述する必要がありますが省略しています。
class ItemDataSource(private val service: QiitaService) : PageKeyedDataSource<Int, Item>() {
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {}

    // API呼び出しをしているので、本来であればここで例外の対処を記述する必要がありますが省略しています。
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>) {
        // 1 ページ目のデータを取得する
        val page = 1

        // 1 ページに表示するデータ数
        val perPage = params.requestedLoadSize

        // ページに表示するデータを取得する
        val items = service.getItems(page, perPage).execute().body() ?: emptyList()

        // 次に表示するページの番号を計算する
        val nextPage = page + 1

        // 取得したデータ、次に表示するページの番号を結果として返す
        callback.onResult(items , null, nextPage)
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
        // params.key には 前の loadInitial や loadAfter の呼び出しで返した nextPage が格納されている
        val page = params.key // 1 ページ目のデータを取得する

        // params.requestedLoadSize には 1ページに表示するデータ数が格納されている。
        val perPage = params.requestedLoadSize

        // ページに表示するデータを取得する
        val items = service.getItems(page, perPage).execute().body() ?: emptyList()

        // 次に表示するページの番号を計算する
        val nextPage = page + 1

        // 取得したデータ、次に表示するページの番号を結果として返す
        callback.onResult(items, nextPage)
    }
}
```

**ItemDataSourceFactory**

DataSource にデータ取得の処理を記述しましたが、DataSource.Factory を通して生成するような仕組みになっています。実際に LivePagedListBuilder で利用するのは DataSource.Factory になりますので定義してやります。

```kotlin
class ItemDataSourceFactory(service: QiitaService) : DataSource.Factory<Int, Item>() {
    private val source = ItemDataSource(service)

    override fun create(): DataSource<Int, Item> {
        return source
    }
}
```

**MainViewModel**

ここまで準備できればあとは PagedList を作成するだけです。作成した ItemDataSourceFactory を DI して LivePagedListBuilder に渡します。そして build してやれば PagedList が作成されます。これらのオブジェクトの作成は次のように MainViewModel を行うことにします。

```kotlin
class MainViewModel(private val itemDataSourcefactory: ItemDataSourceFactory): ViewModel() { 
    private val config = PagedList.Config.Builder().setInitialLoadSizeHint(10).setPageSize(10).build()
    val entities: LiveData<PagedList<Item>> = LivePagedListBuilder(factory, config).build()
}
```

# Step 3 EpoxyRecyclerView で表示する EpoxyModel を作成する

EpoxyRecyclerView で表示する EpoxyModel を作成していきます。
次のような TextView を持つ CardView というシンプルなレイアウトを今回は定義します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="title"
            type="String" />

    </data>

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        app:cardCornerRadius="4dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="@{title}"
            android:textSize="24sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="TEST TEST TEST TEST TEST" />

    </androidx.cardview.widget.CardView>
</layout>
```

あと package-info.java というファイルをどこでもいいので作成して、EpoxyDataBindingLayouts に先程作成してレイアウトファイルの名称を記述してやります。このファイルを記述することで Epoxy が定義したレイアウトファイルから自動的に EpoxyModel を生成してくれるようになります。

```java
@EpoxyDataBindingLayouts({R.layout.layout_epoxy_item } )
package jp.kaleidot725.sample;

import com.airbnb.epoxy.EpoxyDataBindingLayouts;
```

# Step4 EpoxyRecyclerView で表示を制御する EpoxyController を作成する

Epoxy で Paging をしたいときには PagedListEpoxyController を継承したクラスを作成する必要があります。PagedListEpoxyController では buildItemModel にてページごとに表示する EpoxyModel を生成するようになっています。なので buildItemModel で Step3 で定義した EpoxyModel を生成して返すようにしてやります。

```kotlin
class ItemPagedListController : PagedListEpoxyController<Item>() {
    override fun buildItemModel(currentPosition: Int, item: Item?): EpoxyModel<*> {
        requireNotNull(item)
        return LayoutEpoxyItemBindingModel_().apply {
            id(item.id)
            title(item.title)
        }
    }
}
```

# Step 5 作成したクラスの動作を確認する

あとは今まで作成したクラスを初期化してセットアップしてやるだけです。
次の定義で QiitaService・ItemDataSourceFactory・MainViewModel を生成できるようにしてやります。

**Koin**

```kotlin
object ViewModelFactory {
    private val retrofit get() = Retrofit.Builder()
            .baseUrl("https://qiita.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    private val service = retrofit.create(QiitaService::class.java)

    val mainViewModel: MainViewModel get() = MainViewModel(ItemDataSourceFactory(service))
}
```

そして MainActivity にて ViewModel や EpoxyRecyclerView のセットアップをしてやれば OK です。

**MainAcitivity**

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).also { binding ->
            val controller = ItemPagedListController()
            binding.epoxyRecyclerView.adapter = controller.adapter

            val layoutManager = LinearLayoutManager(applicationContext).apply { orientation = RecyclerView.VERTICAL }
            binding.epoxyRecyclerView.layoutManager = layoutManager

            val viewModel = ViewModelFactory.mainViewModel
            binding.viewModel = viewModel
            viewModel.entities.observe(this, Observer { controller.submitList(it) })
        }
    }
}

```

**activity_main.xml**

```xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="viewModel"
            type="jp.kaleidot725.sample.ui.MainViewModel" />
    </data>

    <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="match_parent"
        tools:context=".ui.MainActivity">

        <com.airbnb.epoxy.EpoxyRecyclerView
            android:id="@+id/epoxy_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
</layout>
```

# おわりに

Epoxy だと PagedListEpoxyController を継承した Controller を実装してやれば Paging に対応できる感じになっているみたいですね。Paging の実装の部分に関しては特に通常の RecyclerView を使っていた実装と変わらずそのまま使えるみたいですね。

<a href="https://github.com/kaleidot725-android/epoxy/tree/master/src/paging"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/kaleidot725-android/epoxy.png" width="460px"></a>