N予備校 Androidチームの鎌田です。
N予備校 Android チームでは、アプリの各通信処理をRetrofitに移行しました。
本記事では、コード例を交えてこれまでの実装からどのように Retrofit に移行したかの経緯、移行にあたってつまづいたポイントについてまとめます。
また、Retrofit の細かい解説に関してはしませんのでご承知ください。
これまでの実装
これまでは、OkHttp をベースにした独自のApiClient
クラスを作成し、以下のように実装していました。
class ExampleRepository(private val apiClient: ApiClient) {
fun postExampleInfo(id: String, name: String): Single<ExampleData> {
apiClient.post("example/$id/info", mapOf("name" to name)) { jsonObject ->
// JSONをExampleDataに変換する。
ExampleData.from(jsonObject)
}
}
}
そして、以下のようにJSONObject
からデータクラスに変換していました。
data class ExampleData(data: String) {
companion object {
fun from(jsonObject: JSONObject): ExampleData {
return ExampleData(data = jsonObject.getString("data"))
}
}
}
上記のデメリットとしては以下があると考えています。
- JSON からデータクラスへの変換処理を実装しないといけない
- テストコードも必要になりコード量が増える
- 実装から API の仕様が読み取りにくい
Retrofitへの移行にあたって
Retrofit への移行にあたって、チーム内での知見が少なかったため、以下の Codelab であらかじめ学習しました。
https://developer.android.com/codelabs/basic-android-kotlin-training-getting-data-internet#0
JSON ライブラリとして、Retrofit にデフォルトで対応していて、かつ比較的パフォーマンスが高いMoshiを採用しました。
エラーハンドリングの実装
元々ApiClient
内部でエラーハンドリングを共通化しており、それを実現するために以下のサンプルコードを参考にエラーハンドリングを実装しました。
実装としては以下のような感じです。
interface MyCall<T : Any> {
fun cancel()
fun clone(): MyCall<T>
@Throws(IOException::class)
fun execute(): Single<T>
}
class MyCallImpl<T : Any>(
private val call: Call<T>
) : MyCall<T> {
override fun cancel() {
call.cancel()
}
override fun clone(): MyCall<T> {
return MyCallImpl(call.clone())
}
override fun execute(): Single<T> {
var response: Response<T>? = null
return Single.create { subscriber ->
try {
response = call.clone().execute()
subscriber.onSuccess(response!!)
} catch (e: Exception) {
subscriber.onError(e)
}
}
.retryWhen { // 通信エラー時のエラーハンドリングをここで行う }
.flatMap { Single.just(response!!.body()) }
.retryWhen { // レスポンスが空だった時、ステータスコードに応じたエラーハンドリングをここで行う }
.subscribeOn(Schedulers.newThread())
}
}
サンプルコードではenqueue
メソッドをオーバーライドしていましたが、もともとexecute
メソッドを使用していたのでこちらをオーバーライドしています。
class MyCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != MyCall::class.java) {
return null
}
check(returnType is ParameterizedType) { "MyCall must have generic type (e.g., MyCall<ResponseBody>)" }
val responseType = getParameterUpperBound(0, returnType)
return MyCallAdapter<Any>(responseType)
}
private class MyCallAdapter<T : Any>(
private val responseType: Type
) : CallAdapter<T, MyCall<T>> {
override fun responseType(): Type {
return responseType
}
override fun adapt(call: Call<T>): MyCall<T> {
return MyCallImpl(call)
}
}
}
CallAdapter
周りに関してはほぼサンプルコードを流用しました。
上記の処理を反映させるため、Retrofit のインスタンスを生成する際には以下のように実装しました。
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(MyCallAdapterFactory())
.baseUrl(baseUrl)
.build()
そして、通信処理を以下のように置き換えました。
interface ExampleService {
@POST("example/{id}/info")
fun postExampleInfo(@Path("id") id: String, @Body body: Map<String, String>):
}
data class ExampleData(
@Json(name = "data")
val data: String
)
class ExampleRepository(private val exampleService: ExampleService) {
fun postExampleInfo(id: String, name: String): Single<ExampleData> {
exampleService.postExampleInfo(id = id, body = mapOf("name" to name)).execute()
}
}
これにより、Retrofit に置き換えることができました。
つまづいたポイント
レスポンスの内容に応じて変換したいデータクラスが変わる場合
つまづいたポイントとして、レスポンスの内容に応じて変換したいデータクラスが変わる場合です。
どちらのクラスに変換するべきか Moshi 側では判断できないためです。
例えば以下のような場合です。
sealed class BaseData
data class FirstData(val data: String): BaseData()
data class SecondData(val data: String): BaseData()
その場合、以下のように一時的なデータクラスを用意します。
data class DataResponse(
@Json(name = "type")
val type: String,
@Json(name = "data")
val data: String
)
例えばtype
に応じてデータクラスを変える場合、以下のようなメソッドを実装します。
fun from(dataResponse: DataResponse): BaseData {
return when (dataResponse.type) {
"first" -> FirstData(data = dataResponse.data)
"second" -> SecondData(data = dataResponse.data)
else -> throw IllegalStateException()
}
}
変換処理が増えてしまうのがデメリットではありますが、既存コードの影響範囲を最小限に抑えることができました。
まとめ
Retrofit に移行したことで、コード量が減って可読性が上がりました。
現時点では RxJava ですが、今後 Kotlin Coroutine, Flow にも対応する予定なので、対応もしやすくなりました。