Android
Kotlin
coroutine
Retrofit

Kotlin coroutines + Retrofit 2 で API 通信を実装する


注意

この記事は、Kotlin: v1.2.31, Coroutines: v0.22.5に基づいており、最新バージョンでは正常に動作するものではないので、ご注意ください。


まえがき

みなさん Kotlin coroutines 使ってますか。

Kotlin coroutines の入門は、こちらから。

入門Kotlin coroutines

Android アプリの API 通信処理は、SDK 標準の AsynTask/Loader が使いづらかったため、近年は RxJava + Retrofit で実装することが主流になっていると思います。

が、上記記事にも書かれていますが、RxJava は非同期処理を行うためのフレームワークでもなんでもなく、単に API 通信時の非同期処理を行うだけに使うのは、学習コストが少し高かったりします。

そんな中、Kotlin coroutines が、次期バージョンで experimental が外れそうなこともあり、Kotlin coroutines + Retrofit という組み合わせも選択肢にあがってくると思われるので、サンプルアプリで実装を試してみます。


サンプルアプリ

retrofit2-kotlin-coroutines-sample

Trello API を使用して、カードの一覧を表示するだけの簡単なアプリです。

この記事では、このコードを元にしています。


やってみる


準備

coroutines + Okhttp + Retrofit + Gson の組み合わせで実装しています。


build.gradle

dependencies {

def coroutines_version = '0.22.5'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

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

def retrofit_version = '2.4.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:1.0.0'

implementation 'com.google.code.gson:gson:2.8.2'
}


Rx + Retrofit の組み合わせでも、interface の定義で、Single や Completable が使えるように、Adapter を使用していましたが、coroutines でも同じく Adapter を使います。

Jake 作の retrofit2-kotlin-coroutines-adapter を使用しています。

まずは、 retrofit2-kotlin-coroutines-adapter を Retrofit に登録します。


NetworkModule.kt

Retrofit.Builder()

.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(CoroutineCallAdapterFactory()) // Adapter を登録
.client(client)
.build()


API 通信処理

では、実際にコルーチンを使って実装していきます。

Trello のボードの一覧を取得する処理を見てみます。

まずは、API の interface 定義。

Rx で Single<List<Board>> と書いていたのと同じように Deferred<List<Board>> を戻り値として定義します。


BoardsApi.kt

@GET("members/{name}/boards")

fun getBoards(
@Path("name") userName: String,
@Query("key") key: String,
@Query("token") token: String
): Deferred<List<Board>>

上記で定義したメソッドを UI 層の Presenter から呼び出します。

呼び出しから非同期処理 (コルーチンでいう中断される処理) まで、まとめて見ていきます。


BoardListPresenter.kt

class BoardListPresenter @Inject constructor(

private val view: BoardListActivity,
private val useCase: BoardUseCase
) {

private val job = Job()

fun onViewCreated() {
launch(job + UI) { loadPipelines() }
}

fun onDestroy() {
job.cancel()
}

private suspend fun loadPipelines() {
try {
useCase.getMyBoards().let(view::addBoards)
} catch (t: Throwable) {
t.message?.let(view::showErrorToast)
}
}
}



BoardUseCase.kt

suspend fun getMyBoards(): List<Board> = withContext(CommonPool) {

membersApi.getBoards("me", API_KEY, ACCESS_TOKEN).await()
}

launch(job + UI) {} で UI スレッドのコルーチンを生成し、 Job に登録します。

ここで登録した Job は onDestory でキャンセルしています。

BoardUseCase.getMyBoards() は API リクエストを投げる suspend fun になっていて withContext(CommonPool) {} で非同期処理を行うスレッドを指定しています。


API エラーハンドリング

エラー処理は、try/catch で行います。ここで渡される Throwable は、Retrofit で返される Throwable が流れてきます。

実際にコードを書く際は、エラー内容によってエラー処理を分けることになりますが、以下のようにハンドリングできます。

これは、Rx で書いても同じですが。


Error.kt

try {

// API request
} catch (t: Throwable) {
when(t) {
is IOException -> { // Network error }
is HttpException -> {
when(t.code()) {
HttpURLConnection.HTTP_BAD_REQUEST -> { }
HttpURLConnection.HTTP_INTERNAL_ERROR -> { }
}
}
}
}


Completable のようなもの

削除 API などでは、オブジェクトを返さないこともあります。

Rx では、そういったストリームを Completable で扱いますが、コルーチンで扱う場合は、以下のように Deferred<Unit> で定義することができます。


CardsApi.kt

@DELETE("cards/{id}")

fun deleteCard(
@Path("id") cardId: String,
@Query("key") key: String,
@Query("token") token: String
): Deferred<Unit>

コルーチンにおいて本来であれば、Deferred<Unit>Job で扱う方が素直ですが、今回、使用した Adapter が Job を扱えないので、 Deferred<Unit> を使用しています。


おわりに

次は、 AAC の LiveData + Retrofit + コルーチンの組み合わせも試してみたいことろです。

あ、まだ Kotlin coroutines は experimental です。