LoginSignup
17

More than 3 years have passed since last update.

Epoxy+Paging+Coroutine+Dagger2+Retrofit2 etc...でAndroidアプリ作ってみた

Last updated at Posted at 2019-10-19

表題の通り、色々なライブラリを使用してAndroidアプリを作ってみました。

はじめに

初めまして、ほりすです!
現在大学院を休学してAndroidエンジニアになるべく、東京でインターンに励んでいます。

今回、初めてQiitaを書くので、細かいところでおかしな点があるかもしれませんが、その時はご指摘願います。

ちなみに、気合いいれて書きすぎて分量が卒論レベルになってしまったので、承知の上でご覧ください

目的

2週間前に参画し始めたインターンシップで担当している技術範囲(特にEpoxy,Paging)のキャッチアップをするために、サンプルアプリを作成しました。

詳細は後述しますが、今回作ったアプリはAPIを叩いてリスト表示するだけの簡単な機能要件です。
ですが、EpoxyとPagingを組み合わせて作成されたサンプルや記事が以外とすくなかったのと、同じ学生Androidエンジニアとの情報共有をするために体系的に記事にまとめようというところでQiitaを書くに至りました。

コードはho2ri2s/githubにあります。
プルリクもガンガン受け付けているので是非!

やること

・2019年10月現在で主流のKotlinやAndroidのライブラリを使用したアプリを作成し、流れを掴む
 ・Http通信や非同期処理、DIやリスト表示の便利なライブラリなどを使用しています。
・そのライブラリを使う目的の自分なりの解釈
・実装方法さらっと流す

やらないこと

・設計の詳細説明
・各ライブラリの詳細説明

作成したアプリ

今回作成したサンプルはこちらです。

githubAPIを用いてユーザープロフィールをカルーセルで表示し、カルーセルのアイテムをタップすると、そのユーザーのリポジトリをリスト表示するといった内容です。

時間があればタップ時にアイテムをハイライトさせたり、URLをタップするとそのページに飛べるようにしたいのですが、現段階である程度形になったので、まとめようと思います。

実装 

設計

今回はチームではなくて個人開発ですし、リリースするわけでもないので最適な設計ということは考えていませんが、学習のためにMVVMを使用しました。
DBは使用しておらず、ドメインと呼ぶほど大げさなものはないのでRepositoryやUseCaseは導入していません。

厳密にアーキテクチャを考えたわけではありませんが、ファイル階層は以下のようになります。
詳しくはgithubでご覧ください。

使用ライブラリ一覧

Epoxy
RecyclerView周り
Paging
データ取得&リスト表示周り
Coroutine
非同期処理周り
Dagger2
Dependency Injection(DI)
Retrofit2
Http通信周り
OkHttp
Http通信周り
Moshi
JSON->Kotlin(Java)変換
Glide
画像読み込み
LiveData&ViewModel
ライフサイクル管理等
DataBinding
viewとデータの結びつけ

Epoxy

apply plugin: 'kotlin-kapt'

kapt{
    correctErrorTypes = true
}
dependencies {
    def epoxy_version = '3.8.0'

    implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
    implementation "com.airbnb.android:epoxy:$epoxy_version"
    implementation "com.airbnb.android:epoxy-databinding:$epoxy_version"
    kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
}

EpoxyはAirbnb製の、RecyclerViewの実装を簡単にしてくれる便利なライブラリです。
簡単にとはどういうことかというと、

このAirbnbのAndroidアプリのように、縦長スクロールに様々なViewTypeが内包されているケースで力を発揮します。

この画面だと、縦長スクロールの中に横スクロールのカルーセルがあり、その下にTextやButtonなどが内包されたCardがあったり。
画像では見えていませんが、さらに下まで行くとGridで表示されたリストも存在します。

これらを生のRecyclerViewやListViewで記載しようとすると、辛いものがあるので、Epoxyの出番というわけです。

後述しますが、リストであっても、リストでなくても、それぞれのxmlと対応するModelを作り、それらを1つのControllerでいい感じにまとめて描画してくれるのが好きなポイントです。

他にもGroupieという同じくRecyclerViewを楽に実装できるライブラリがあるので、興味のある方は検索してみてください。

Paging

    def paging_version = '2.1.0'
    implementation "androidx.paging:paging-runtime:$paging_version"

Pagingは、Android JetpackのAAC(Android Architecture Components)に含まれるライブラリです。

The Paging Library helps you load and display small chunks of data at a time. Loading partial data on demand reduces usage of network bandwidth and system resources.

とかかれてある通り、APIを叩く際に一気に全てのデータを取得するのではなく、小さく小分けにして取得するのに適しているライブラリです。そうすることでデータの使用量も抑えられ、簡単に無限スクロールを実現することもできるのです。

Kotlin Coroutine

    def coroutine_version = "1.3.2"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"

Coroutinは、よく

非同期ライブラリではなくて、中断可能な計算インスタンスです

と言われているのですが、自分もなかなか細部まで理解はできていないところがあります。
わかりやすくまとめてくださっている記事/スライドがあるので、詳しく知りたい方はこちら参照ください。
Kotlin コルーチンを理解しよう
図で理解する Kotlin Coroutine

今回のサンプルアプリでは、viewModelScopeを用いてViewModelのScope内でHttp通信をして、ViewModelが破棄されたらそのJOBを切れるように...くらいの用途でしか使えていません。
(viewModelScopeは厳密にはLifecycleライブラリのもの)

Dagger2

    def dagger_version = "2.24"
    implementation "com.google.dagger:dagger:$dagger_version"
    //kaptのpluginはEpoxyで既に記載済み
    kapt "com.google.dagger:dagger-compiler:$dagger_version"

Dagger2は依存性を注入する(Dependency Injection)のためのライブラリです。
これまた厄介で、すごく奥が深いので自分はあんざいゆきさんのMaster of Daggerなどで勉強しています。
簡単にいうと、具象ではなく抽象に依存したり、シングルトンであるインスタンスを使い回ししやすく、テストがしやすくなるようなライブラリです。

ただ、Dagger2は学習コストが高いので、無理に使う必要もないですし、他にもKoinやKodein等のDIライブラリがあります。
今回は担当のプロジェクトがDagger2を使用しているので学習のために導入しました。
(まだテストを書いていないのでうまみが少ないですが...)

Retrofit2

    def retrofit_version = "2.6.2"
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
    //Coroutineを使うなら
    implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"


RetrofitはHTTPクライアント用のライブラリです。
返り値を持った関数にアノテーションをつけてあげることで、APIをシンプルに叩くことができます。
Coroutineにも対応しており、Deferredを返り値に指定してあげることができます(今回はしていませんが)

後述のOkHttpとMoshiと相性が良く、一緒に使われることが一般的です。

OkHttp

    def okhttp_version = '4.2.2'
    implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
    implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"

上記のRetrofit2を開発しているSquare社のHTTPクライアントライブラリです。
こちらも非常に便利なライブラリで、接続限度時間を設定できたり、通信状況が悪い時は再接続してくれたりします。
また、通信したURLや、ステータスコード、受け取ったレスポンスなどをログに出してくれるロギング機能もあるため、通信周りで不具合が生じたときにどんな状況なのかを把握しやすいのも便利です。

こちらは、Retrofitインスタンスを作成するときにclientを渡せるため、上記の諸々の設定をしてあげたOkHttpのクライアントを渡してあげます。

Moshi

    def moshi_version = '1.8.0'
    implementation "com.squareup.moshi:moshi:$moshi_version"
    implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"

Moshiは、レスポンスで受け取ったJSONを、Kotlinで扱いやすいようにObjectに変換してくれるライブラリです。

他にもGSONが広く使われていますが、KotlinではMoshiの方が相性が良いらしいです。
詳しくは下記記事参照です。
Kotlinと相性が良いMoshiのkotlin extensionを使う

Glide

    def glide_version = "4.10.0"
    implementation "com.github.bumptech.glide:glide:$glide_version"

Glideはurlから画像を読み込んでViewに表示するのに便利なライブラリです。
色々便利な用途はあるそうですが、今回の用途はこれに止まっています。
まあ、InputStreamあたりを使わなくていいので、所謂ボイラープレートコードが減り、完結に書くことができるので好きです。

LiveData & ViewModel

     def lifecycle_version = '2.2.0-beta01'
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

これらも、AACのライブラリ群の中の1つで、Androidでの面倒臭いLifecycleをよしなに管理してくれる便利なライブラリです。
viewModelScopeを使いたいので、viewmodel-ktxもgradleに追加しています。

この辺りは、MVVMを使うのであればほぼ必須レベルで便利なので、逆にこれらを使わない実装ができなくなってきた...

DataBinding

android{
    dataBinding{
        enabled = true
    }
}

DataBindingは、MVVMと相性の良い技術で、レイアウトのViewとオブジェクトのデータを紐づける機能です。

イメージ的には、DataBindingを使わない場合は、

のような感じで、Activity等を経由して、ViewModelが持つデータをlayoutに紐づけるイメージですが、

DataBindingを使う場合、

と、layoutとViewModel等を直接結びつけられるイメージです。

※あくまで自分なりのイメージなので、厳密には違うかもです。

これでtextName.text = viewModel.nameなどのボイラープレートコードが減ってスッキリします。
また、BindingAdapterを使うことによって、さらに見やすいコードにすることができます。

実装の流れ

お待たせしました。ライブラリ紹介が長くなってしまいましたが、実装に移っていきます。

自分は学習のため、
DI設定->data層実装->Epoxy/Pagingなしでリスト表示->Paging導入->Epoxy導入
の順序で実装していきました。

実装の順番は好みと場合によると思うので、今回紹介するのはあくまで自分のやり方です。ご自身の好きな順序、方法でどうぞ。

DI

今回はDagger2の詳細な実装方法に関しては解説しません。
Daggerに関してあまり知識がない方はAndroid Dagger codelabMaster of Daggerなどで知見をつけるとよいかもしれません。

わからない技術が出てきたときには、読み飛ばすのではなくて「へー、そんなものがあるんだ」程度で眺めておくと、次回出てきたときに頭に入りやすいのかなーと思います。

では、実装にうつります。

ViewModelの注入

まず、ViewModelをActivity(もしくはFragment)に注入するためのクラスを作成します。

ViewModelKey.kt
@MustBeDocumented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
ViewModelFactory.kt
class ViewModelFactory @Inject constructor(
    private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        var creator: Provider<ViewModel>? = creators[modelClass]
        if (creator == null) {
            for ((key, value) in creators) {
                if (modelClass.isAssignableFrom(key)){
                    creator = value
                    break
                }
            }
        }
        if(creator == null)throw IllegalArgumentException("unknown model class $modelClass")
        try {
            return creator.get() as T
        }catch (e: Exception){
            throw RuntimeException(e)
        }
    }
}
GithubViewModelModule.kt

@Module
abstract class GithubViewModelModule {

    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(GithubViewModel::class)
    abstract fun bindGithubViewModel(viewModel: GithubViewModel): ViewModel
}

ViewModelModule,ViewModelKey,ViewModelFactoryを作成。
ViewModelの注入方法はいくつかあるとのことですが、自分はこの方法でいつもやっています。

Retrofit(apiインターフェース)の注入

今回はViewModelからAPIを叩くため、apiを注入する用のDataModuleを作成します。

DataModule.kt
@Module
class DataModule {

    @Singleton
    private val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(120, TimeUnit.SECONDS)
        .readTimeout(120, TimeUnit.SECONDS)
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .build()

    @Singleton
    private val moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()

    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .build()

    @Singleton
    @Provides
    fun provideGithubApi(retrofit: Retrofit): GithubApi =
        retrofit.create(GithubApi::class.java)

    companion object {
        private const val BASE_URL = "https://api.github.com"
    }
}

まず、Okhttpライブラリを使用してTimeOutの時間設定や、ロギング設定をし、MoshiでJSONをKotlinオブジェクトに変換できるようにします。
そしてそれらをretrofitのBuilderに詰め込んで、GithubApiインターフェースを返すprovideGithubApiを作成します。

今回はgithubAPIしか使用しませんが、いくつもAPIがある場合にはbaseUrlも異なるため、いきなりGithubApiインターフェースを返すのではなく、Retrofit.Builderと分けています。
(そう考えるとmoshiとclientも関数の返り値として@Providesした方がいいのかな...?)

AppComponent & App

AppCompoenent.kt
@Singleton
@Component(
    modules = [
        GithubViewModelModule::class,
        DataModule::class
    ]
)
interface AppComponent {

    @Component.Builder
    interface Builder {
        fun build(): AppComponent
    }

    fun inject(activity: MainActivity): MainActivity
}
App.kt
class App: Application(){

    lateinit var appComponent: AppComponent
    override fun onCreate() {
        super.onCreate()
        appComponent = DaggerAppComponent.builder().build()
    }
}

これまでに作成した2つのModuleをAppComponentに指定してあげることで、Moduleで@Bindsもしくは@Providesアノテーションをつけて指定した型をオブジェクトグラフに登録します。
簡単に言うと、インスタンスの使用元からある型[T]をDIにより呼び出そうとしたときは、Moduleで作成した関数の返り値[T]のインスタンスをDaggerが提供するよってことです(だと認識している)。

Http通信

Model.kt
data class Owner (
    val id: String,
    @Json(name = "login")
    val name: String,
    @Json(name = "avatar_url")
    val icon: String
)
data class Repo (
    val owner: Owner,
    @Json(name = "name")
    val repoName: String,
    @Json(name = "html_url")
    val url: String,
    @Json(name = "stargazers_count")
    val star: String
)

githubAPIから返ってくるレスポンスのデータクラスです。

GithubApi.kt
interface GithubApi {

    @GET("/users/{username}/repos")
    suspend fun getGithubRepos(
        @Path("username") username: String,
        @Query("page") page: String,
        @Query("per_page") perPage: String
    ): Response<List<Repo>>

    @GET("/users/{username}")
    suspend fun getOwner(@Path("username") username: String): Response<Owner>
}

Retrofitを用いてHttp通信をするためのメソッド群です。
UserのRepositoryのリスト表示にはPagingを利用するため、引数に"page"と"per_page"を追加しています。
githubではデフォルトでは30個のアイテムを返すのですが、それをper_pageで指定する感じです。
githubAPIのドキュメントに丁寧に記載あるので詳しくはそちらを。

ここで、ブロック関数にするためにsuspend funにしています。

Paging

Paging実装のざっくりした流れとしては、
① APIを叩くインターフェースでpageとper_papeをクエリとして渡す
② PagingライブラリのDataSourceを継承したクラスを作成。
DataSourceは直接インスタンス化せず、Factoryを経由するため、Factoryクラスを作成。
④ ViewModelで③で作成したFactoryを用いてLiveDataなPagedListを持つ。
⑤ ActivityやFragmentでLiveDataをobserveし、変更をEpoxyのControllerに渡す。(Epoxyを使わない場合はRecyclerViewのAdapter)

GithubPagingDataSource

①に関してはHttp通信の項目で説明したので、②からです。
DataSourceには PageKeyedDataSource , ItemKeyedDataSource , PositionalDataSourceの3種類が提供されているのですが、今回はPageKeyedDataSourceを使います。

それぞれ、名前の通り、Page単位で取得するか、ItemのIDなどを起点(key)として取得するか、何番目以降のものを取得するか、という感じなので、用途によって使い分けましょう。

GithubPagingDataSource.kt
class GithubPagingDataSource(
    private val username: String,
    private val api: GithubApi,
    private val scope: CoroutineScope
) : PageKeyedDataSource<Int, Repo>() {
    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, Repo>
    ) {
        scope.launch {
            callApi(1, params.requestedLoadSize) { next, repos ->
                callback.onResult(repos, null, next)
            }
        }
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Repo>) {
        scope.launch {
            callApi(params.key, params.requestedLoadSize) { next, repos ->
                callback.onResult(repos, next)
            }
        }
    }

    // 途中から読み出すことはないので実装せず
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Repo>) {
    }

少し長めなので分割して解説します。
PageKeyedDataSourceを継承したクラスはloadInitialloadBeforeloadAfterをoverrideする必要があります。
それぞれ初めのページ、前のページ、次のページを取得するメソッドです。そのままですね。

ここでCoroutineScopeを作成してもいいのですが、今回はコンストラクタにCoroutineScopeを引数にとり、GithubViewModelでviewModelScopeを渡しています。
その場合、invalidateメソッドをオーバーライドして、データの変更などで読み込みが中断されたときにscopeのJOBをキャンセルしてあげます。

GithubPagingDataSource.kt
    override fun invalidate() {
        super.invalidate()
        scope.cancel()
    }

callApiメソッドではapiを叩いてResponseを取得し、Link headerにnextがあれば次のページをkeyとしてcallbackに渡すということをしています。

GithubPagingDataSource.kt
    private suspend fun callApi(
        page: Int,
        perPage: Int,
        callback: (next: Int?, repos: List<Repo>) -> Unit
    ) {
        try {
            val response = api.getGithubRepos(username, page, perPage)
            if (response.isSuccessful) {
                response.body()?.let {
                    var next: Int? = null
                    //Headerにnextがあれば次ページを加算
                    response.headers().get("Link")?.let { value ->
                        val regex = Regex("rel=\"next\"")
                        if (regex.containsMatchIn(value)) {
                            next = page + 1
                        }
                    }
                    callback(next, it)
                }
            }
        } catch (t: Throwable) {
            t.printStackTrace()
        }
    }

DataSourceFactory

DataSourceFactory.kt
class DataSourceFactory(
    private val username: String,
    private val api: GithubApi,
    private val scope: CoroutineScope
) : DataSource.Factory<Int, Repo>() {

    override fun create(): DataSource<Int, Repo> = GithubPagingDataSource(username, api, scope)
}

③に該当する箇所です。
特筆すべきところはなく、単純に先ほど作成したDataSourceを返すFactoryクラスです。

④、⑤に関しては便宜上Epoxy紹介後に解説します。

Epoxy

続いて、Epoxyです。
Epoxyの実装のざっくりした流れは、

① list表示したいアイテム等のレイアウトを作成
② ①のレイアウトに対応するModelを作成
③ ①~②を任意の回数繰り返し、ControllerでModelを構築
④ ActivityやFragmentでRecyclerViewのadapterにcontrollerのadapterをセットする。

と言う感じです。
実際にやってみます。

Layout

①です。
レイアウトのコードはgithubを参照ください。

Model

OwnerModelとRepositoryModelはほぼ同じなので、OwnerModelのみ掲載します。

OwnerModel.kt
@EpoxyModelClass(layout = R.layout.item_owner)
abstract class OwnerModel : EpoxyModelWithHolder<OwnerModel.OwnerViewHolder>() {

    @EpoxyAttribute
    lateinit var owner: Owner

    @EpoxyAttribute
    lateinit var cardClickListener: (String) -> Unit

    override fun bind(holder: OwnerViewHolder) {
        super.bind(holder)
        holder.binding.owner = owner
        holder.binding.containerCard.setOnClickListener{
            cardClickListener(owner.name)
        }
    }

    inner class OwnerViewHolder : EpoxyHolder() {
        override fun bindView(itemView: View) {
            binding = DataBindingUtil.bind(itemView) ?: run {
                throw IllegalStateException("Cannot create binding.")
            }
        }

        lateinit var binding: ItemOwnerBinding
    }
}

@EpoxyModelClassアノテーションで、どのレイアウトのModelクラスなのかを指定します。

ここで、abstract classにするのは、実際にControllerでModelを構築するときに使用するHogeModel_クラスが、HogeModelを拡張して自動生成されるからです。

あとは、RecyclerViewのAdapterと似ていて、ViewHolderを用意してあげて、overrideしたbindメソッドでDataBindしてあげる感じになります。
※R.layout.item_ownerではdatabindingのvariableでOwnerクラスのownerを定義しています。

1点@EpoxyAttributeアノテーションをつけた変数が、lateinit varなのに何も代入していないのが気になると思います。ですが、以下のように自動生成されたjavaコードの中でちゃんとsetするメソッドがあり、後ほどControllerでModelを構築する際にsetしてあげるので、一旦無視して構いません。

OwnerModel_.java
  public OwnerModel_ owner(@NotNull Owner owner) {
    onMutation();
    super.owner = owner;
    return this;
  }

Controller

GithubController.kt
class GithubController(
   private val listener: CardClickListener
): PagedListEpoxyController<Repo>() {

    interface CardClickListener {
        fun onClickCard(userName: String)
    }

    var owners: List<Owner> = emptyList()

    override fun buildItemModel(currentPosition: Int, item: Repo?): EpoxyModel<*> {
        requireNotNull(item)
        return RepositoryModel_().apply {
            id(currentPosition)
            repo = item
        }
    }

    override fun addModels(models: List<EpoxyModel<*>>) {
        carousel {
            id("carousel")
            spanSizeOverride { _, _, _ -> 2 }
            paddingDp(8)
            numViewsToShowOnScreen(2f)
            models(
                owners.map {
                    OwnerModel_().apply {
                        id(it.id)
                        owner = it
                        cardClickListener = listener::onClickCard
                    }
                }
            )
        }
        super.addModels(models)
    }
}

通常のEpoxyを利用する方法は今回詳細には触れませんが、異なる点はPagedListEpoxyControllerを継承している点です。これによって、overrideするメソッドの返り値がEpoxyModelとなります。

通常のEpoxyControllerを継承する場合のbuildModel()メソッドは返り値が存在しないため、まとめてModelを構築することができるのですが、Pagingを利用する際は返り値が存在しており、複数Modelを構築するのに向いていません。

では、どのようにして複雑なViewType(カルーセルやリスト、場合によってはヘッダーやフッター等も)の表示順序をコントロールするのでしょうか。
お気づきとは思いますが、そうです。addModels(models: List<EpoxyModel<*>>)です。このメソッド内でどのModelをどういう順番で並べるかを指定できるのです。

今回であれば、Ownerのアイコン/名前を表示するカルーセルの下に、そのリポジトリ一覧が並んで欲しいです。
そのため、カルーセルを作成した後にsuper.addModels(models)としています。
このmodelsは、buildItemModel()でreturnしたEpoxyModelが引数として渡されており、今回はRepositoryModel_です。

この辺りで、
。○(? buildItemModelですでにRepositoryのModelは構築されたんじゃないの...)
って思われる方もいらっしゃるかもしれません。自分は思いました。

この例ではKotlin Extensionsを用いてカルーセルの構築をしているのですが、普通に書くとこんな感じです。

CaroucelModel_()
  .id("caroucel)
  //略
  .addTo(this)

実際は、addTo(this)でModelをControllerにaddしているのです。
よく見てみると、buildItemModelではaddTo(this)がされていないため、Modelの構築は完全にはなされていなかったのです。
super.addModels(models)を呼ぶことで初めてModelが構築されるんですね。

④に関しては、便宜上ViewModel紹介後に解説します。

ViewModel

後もう一踏ん張りです!
ViewModelも長めなので順番に説明していきます。

GithubViewModel.kt
    val repos: LiveData<PagedList<Repo>> =
        switchMap(_name) { username ->
            val dataSourceFactory = DataSourceFactory(username, api, viewModelScope)

            val config = PagedList.Config.Builder()
                .setPageSize(PAGE_SIZE)
                .setInitialLoadSizeHint(PAGE_SIZE)
                .build()

            LivePagedListBuilder(dataSourceFactory, config).build()
        }

    private val _name = MutableLiveData<String>()

    fun setUsername(username: String) = viewModelScope.launch {
        _name.postValue(username)
    }

    companion object {
        private const val PAGE_SIZE = 20
    }

ここは、Pagingの項目で作成したDataSourceを利用してLiveDataなPagedListを作成するところです。

シンプルにやるのであれば、
① DataSourceFactoryのインスタンス化
② PagedListのConfig設定
③ ①,②を使ってLiveDataなPagedListを作成
だけで良いのですが、
今回はクリックイベントを起点に、クエリパラメータの値を変更して再度APIを叩いてリスト表示をしたいので、少し工夫しています。

Activityで、CardClickListenerの実装をしているのですが、そこからsetUsername()を呼び、MutableliveDataな_userに値を入れます。

MainActivity.kt
    //ここはMainActivity
    override fun onClickCard(userName: String) {
        viewModel.setUsername(userName)
    }

すると、その変更を検知して再度①~③が走り、リストが更新される挙動になります。

(アドバイスくれたマヤミトくん@yt8492ありがとう...!)

そしたら、あとはOwnerを取得するメソッドを作成して終わりです。

GithubViewModel.kt
    private val _owner = MutableLiveData<List<Owner>>()
    val owner: LiveData<List<Owner>>
        get() = _owner

    fun getOwner() {
        // 今回はここで決め打ち
        val usernames = listOf("ho2ri2s", "googlesamples", "kotlin", "android", "jetbrains")
        viewModelScope.launch {
            val ownerList = ArrayList<Owner>()
            usernames.forEach { username ->
                try {
                    val response = api.getOwner(username)
                    if (response.isSuccessful && response.body() != null) {
                        ownerList.add(response.body()!!)
                    }
                } catch (t: Throwable) {
                    t.printStackTrace()
                }
            }
            _owner.postValue(ownerList)
            setUsername(usernames[0])
        }
    }

今回はシンプルに決め打ちで5つのアカウントの情報を取得しています。
エラーハンドリングとかはしていないので、特筆すべきポイントはないです。

Activity

いよいよラストです。
ViewModelのInjectとか、DataBindingあたりのお決まりコードは省きます。

MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val controller = GithubController(this)
        val linearLayoutManager = LinearLayoutManager(this)
        binding.recyclerView.apply {
            adapter = controller.adapter
            layoutManager = linearLayoutManager
            addItemDecoration(DividerItemDecoration(this@MainActivity, linearLayoutManager.orientation))
        }
        controller.requestModelBuild()

        viewModel.apply {
            owner.observe(this@MainActivity, Observer {
                controller.owners = it
                controller.requestModelBuild()
            })
            repos.observe(this@MainActivity, Observer {
                controller.submitList(it)
            })
        }
        viewModel.getOwner()
    }

ActivityではControllerをインスタンス化して、RecyclerViewのadapterにControllerのadapterを指定してあげます。
忘れてはいけないのがcontroller.requestModelBuild()です。
これがないと実際にModelが作られません。

あとは、ViewModel側でデータの変更があればそれをControllerに伝えてあげれば完成です。

Epoxyおまけ

activity_main.xmlで、普通のRecyclerViewではなくてEpoxyRecyclerViewとすることもできます。
そうした場合、

val controller = HogeController()
recyclerView.adapter = controller.adapter
controller.requestModelBuild()

をすることなく、

epoxyRecyclerView.setControllerAndBuildModels(HogeController());

とできたり、Controllerを作成せずに

epoxyRecyclerView.withModels {
  header {
     id("header")
     text("hello world)
  }
}

のようにModelを構築することができたりして、スッキリかけます。

さいごに

以上で、githubAPIからユーザーを取得してカルーセルに表示->タップするとそのユーザーのリポジトリリストを得るまでができました。

これらのキャッチアップを始めてまだ日は浅いので、もっと綺麗にかける/こうした方が良い等のマサカリもどんどん投げていただけたら嬉しいです。
また、ここってなんでこういう実装なの?と疑問に思われた方も是非コメントください。一緒に考えましょう!

ほぼ全てのコードを載せたので分量がすんごくなってしまいました
ここまでご覧いただいてありがとうございました。

参考文献

Paging Library + API でページング処理
Android Jetpack 初級 ( Paging library + LiveData + Retrofitで、簡単無限スクロール)
EpoxyでRecyclerViewをもっと簡単にする
RecyclerViewの実装が楽になるEpoxyライブラリを使ってみる
Epoxy入門:複雑な画面をRecyclerViewで楽に作る
EpoxyでRecyclerViewでよく使う機能をサクッと実装する
Simplifying Recycler View with Epoxy in Kotlin — Nachos Tutorial Series
Android Paging Library with Kotlin Coroutines
ZliTechBook

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17