Retrofit 2.9.0 に依存しているアプリに最適化&難読化を適用する

## はじめに

本ドキュメントでは、 Retrofit 2.9.0 に依存している Android プロジェクトにおいて、最適化&難読化を有効にして生成したバイナリで発生する実行エラーを回避する方法について記載する。尚、最適化&難読化は R8 コンパイラによって実施されることを想定している。

最適化&難読化の詳細については以下の公式ドキュメントを参照されたい。

https://developer.android.com/build/shrink-code?hl=ja

## ビルド環境

#### モジュール構成

- app: アプリ本体。 UI 層。    
- domain: ドメイン層  
- data: データ層。 Retrofit を使用。  

#### ビルドツール

AGP: 8.4.1
``` kotlin: build.gradle.kt
plugins {
    id("com.android.application") version "8.4.1" apply false
    id("com.android.library") version "8.4.1" apply false
}
``` 
``` kotlin: data/build.gradle.kt
plugins {
    id("com.android.library")
}
```    
    
Kotlin: 1.9.0

```kotlin
plugins {
    id("org.jetbrains.kotlin.android") version "1.9.0" apply false
}
```    
``` kotlin: data/build.gradle.kt
plugins {
    id("org.jetbrains.kotlin.android")
}
```

#### Retrofit 本体と関連パッケージへの依存を定義

```kotlin: data/build.gradle.kt
dependencies {
    (省略)
    // Retrofit with Moshi Converter
    val retrofitVersion = "2.9.0"
    implementation("com.squareup.retrofit2:converter-moshi:$retrofitVersion")

    // Moshi
    val moshiVersion = "1.15.0"
    implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion")
}
```

#### Retrofit を使った通信プログラム
    
```kotlin: data/datasource/GithubApi.kt
object GithubApi {
    private const val baseUrl = "https://api.github.com/"

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

    private val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(baseUrl)
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .build()

    val githubApiService: GithubApiService by lazy {
        retrofit.create(GithubApiService::class.java)
    }
}
```
    
```kotlin: data/datasource/GithubApiService.kt
interface GithubApiService {
    @GET("search/repositories")
    fun getAllRepo(
        @Query("q") q: String,
        @Query("page") page: Int?,
        @Query("per_page") perPage: Int,
    ): Call<GetAllRepoResponse>
}
```
    
```kotlin: data/pagesource/RepositoryPagingSource.kt
class RepositoryPagingSource(
    private val query: String,
    private val endpoint: GithubApiService
) : PagingSource<Int, RepositoryEntity>() {
(省略)
    private suspend fun fetchRepos(query: String, startKey: Int, page: Int?): RepositoryListEntity? {
        //NOTE: 同期方式の場合はメインスレッド以外で通信する必要あり
        return withContext(Dispatchers.IO) {
            val response = try {
                // 同期方式で HTTP 通信を行う
                endpoint.getAllRepo(query, page, PAGE_SIZE).execute()
            } catch (e: Exception) { // 通信自体が失敗した場合
                val exception = AppException.convertTo(e as Throwable)
                throw exception
            }

            val repositories = if (response.isSuccessful) {
                val responseBody = response.body()
                responseBody?.items?.let { items ->
                    convertToEntity(startKey, items)
                }
            } else {
                val exception = GithubExceptionConverter.convertTo(
                    response.code(),
                    response.errorBody()?.string()
                )
                throw exception
            }

            repositories
        }
    }
(省略)
```
    
```kotlin: data/datasource/model/GetAllRepoResponse.kt
data class GetAllRepoResponse(val items: List<Repository>)
```
    
```kotlin: data/datasource/model/Repository.kt
data class Repository(
    val name: String,
    @Json(name = "full_name") val fullName: String,
    @Json(name = "html_url") val htmlUrl: String,
    val description: String?,
    @Json(name = "created_at") val createdAt: String,
    val owner: Owner
)
```
    

## 手順1: release ビルドで最適化と難読化を有効にする。

まずは app/build.gradle.kt で release ビルドの最適化と難読化を有効にする。

```kotlin: app/build.gradle.kt
     buildTypes {
         getByName("release") {
-            isMinifyEnabled = false
+            isMinifyEnabled = true
             proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+            isShrinkResources = true
         }
     }
```

この時点では proguard-rules.pro 等の ProGuard の設定ファイルには何も設定していない状態であり、この状態でリリースビルドを実行し、生成された .apk でアプリを動かすと以下のステップで `IllegalArgumentException. Unable to create call adapter for interface I3.c
for method c.a` が発生する。

```kotlin
endpoint.getAllRepo(query, page, PAGE_SIZE).execute()
```

## 手順2: Retrofit2 の ProGuard ルールを適用する

[Retrofit の README](https://github.com/square/retrofit?tab=readme-ov-file#r8--proguard) の記載に基づき、[retrofit2.pro](https://github.com/square/retrofit/blob/trunk/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro) を consumer-rules.pro に転記する。または、[retrofit2.pro](https://github.com/square/retrofit/blob/trunk/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro) を data モジュールに配置し、data/build.gradle.kt を以下のように変更しても良い。

```kotlin: data/buidl.gradle.kt
    defaultConfig {
        minSdk = 24
        targetSdk = 34

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
-       consumerProguardFiles("consumer-rules.pro")
+       consumerProguardFiles("consumer-rules.pro", "retrofit2.pro")
    }
```

再度リリースビルドを実行し、生成された .apk でアプリを動かすと今度は以下のステップで `ClassCastException` が発生する。

```kotlin
val responseBody = response.body()
```

#### MEMO
    
data モジュールを Android Studio のウィザードで作った場合は data/buidl.gradle.kt に以下が含まれていることがある。 data/buidl.gradle.kt には `consumerProguardFiles()` だけ存在すれば良いのでこの記述を削除し、data/proguard-rules.pro ファイルも削除する。
    

```kotlin: data/build.gradle.kt
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
```

## 手順3: Retrofit API に設定する **Generics を**ProGuard ルール**に追加する**

`ClassCastException` が発生しているのは `retrofit2.Response<GetAllRepoResponse>` クラスの `body()` メソッドである。よって`GetAllRepoResponse` が難読化されてしまっているのが `Exception` の原因だと考えられる。
そこで、`GetAllRepoResponse` および `Repository` を配置している`dev.seabat.android.pagingarchitectureretrofit.data.datasource.model` パッケージ全てを難読化対象から除外する。( もしかすると `*;` によるフル指定でなくても良いかもしれない。)

```plaintext: data/consumer-rules.pro
-keep class dev.seabat.android.pagingarchitectureretrofit.data.datasource.model.* {
    *;
}
```

再度リリースビルドを実行し、生成された .apk でアプリを起動すると、`ClassCastException` が解消され、最適化＆難読化による実行エラーが発生しなくなる。

## あとがき

Retrofit 2.9.0 を 2.11.0 にアップデートすると 「手順2: Retrofit2 の ProGuard ルールを適用する」 の手順が不要になる。

https://qiita.com/seabat-dev/items/b735400efe8fca044e15