8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Android アプリの各通信処理を Retrofit に移行するためにやったこと

Last updated at Posted at 2022-12-01

N予備校 Androidチームの鎌田です。

N予備校 Android チームでは、アプリの各通信処理をRetrofitに移行しました。

本記事では、コード例を交えてこれまでの実装からどのように Retrofit に移行したかの経緯、移行にあたってつまづいたポイントについてまとめます。

また、Retrofit の細かい解説に関してはしませんのでご承知ください。

これまでの実装

これまでは、OkHttp をベースにした独自のApiClientクラスを作成し、以下のように実装していました。

ExampleRepository.kt
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からデータクラスに変換していました。

ExampleData.kt
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内部でエラーハンドリングを共通化しており、それを実現するために以下のサンプルコードを参考にエラーハンドリングを実装しました。

https://github.com/square/retrofit/blob/master/samples/src/main/java/com/example/retrofit/ErrorHandlingAdapter.java

実装としては以下のような感じです。

MyCall.kt
interface MyCall<T : Any> {
    fun cancel()
    fun clone(): MyCall<T>

    @Throws(IOException::class)
    fun execute(): Single<T>
}
MyCallImpl.kt
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メソッドを使用していたのでこちらをオーバーライドしています。

MyCallAdapterFactory.kt
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()

そして、通信処理を以下のように置き換えました。

ExampleService.kt
interface ExampleService {
    @POST("example/{id}/info")
    fun postExampleInfo(@Path("id") id: String, @Body body: Map<String, String>): 
}
ExampleData.kt
data class ExampleData(
    @Json(name = "data")
    val data: String
)
ExampleRepository.kt
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 側では判断できないためです。

例えば以下のような場合です。

BaseData.kt
sealed class BaseData

data class FirstData(val data: String): BaseData()

data class SecondData(val data: String): BaseData()

その場合、以下のように一時的なデータクラスを用意します。

DataResponse.kt
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 にも対応する予定なので、対応もしやすくなりました。

8
0
0

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
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?