はじめに
本ドキュメントでは、 Retrofit 2.9.0 に依存している Android プロジェクトにおいて、最適化&難読化を有効にして生成したバイナリで発生する実行エラーを回避する方法について記載する。尚、最適化&難読化は R8 コンパイラによって実施されることを想定している。
最適化&難読化の詳細については以下の公式ドキュメントを参照されたい。
ビルド環境
モジュール構成
- app: アプリ本体。 UI 層。
- domain: ドメイン層
- data: データ層。 Retrofit を使用。
ビルドツール
AGP: 8.4.1
plugins {
id("com.android.application") version "8.4.1" apply false
id("com.android.library") version "8.4.1" apply false
}
plugins {
id("com.android.library")
}
Kotlin: 1.9.0
plugins {
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
}
plugins {
id("org.jetbrains.kotlin.android")
}
Retrofit 本体と関連パッケージへの依存を定義
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 を使った通信プログラム
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)
}
}
interface GithubApiService {
@GET("search/repositories")
fun getAllRepo(
@Query("q") q: String,
@Query("page") page: Int?,
@Query("per_page") perPage: Int,
): Call<GetAllRepoResponse>
}
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
}
}
(省略)
data class GetAllRepoResponse(val items: List<Repository>)
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 ビルドの最適化と難読化を有効にする。
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
が発生する。
endpoint.getAllRepo(query, page, PAGE_SIZE).execute()
手順2: Retrofit2 の ProGuard ルールを適用する
Retrofit の README の記載に基づき、retrofit2.pro を consumer-rules.pro に転記する。または、retrofit2.pro を data モジュールに配置し、data/build.gradle.kt を以下のように変更しても良い。
defaultConfig {
minSdk = 24
targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
+ consumerProguardFiles("consumer-rules.pro", "retrofit2.pro")
}
再度リリースビルドを実行し、生成された .apk でアプリを動かすと今度は以下のステップで ClassCastException
が発生する。
val responseBody = response.body()
MEMO
data モジュールを Android Studio のウィザードで作った場合は data/buidl.gradle.kt に以下が含まれていることがある。 data/buidl.gradle.kt には consumerProguardFiles()
だけ存在すれば良いのでこの記述を削除し、data/proguard-rules.pro ファイルも削除する。
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
パッケージ全てを難読化対象から除外する。( もしかすると *;
によるフル指定でなくても良いかもしれない。)
-keep class dev.seabat.android.pagingarchitectureretrofit.data.datasource.model.* {
*;
}
再度リリースビルドを実行し、生成された .apk でアプリを起動すると、ClassCastException
が解消され、最適化&難読化による実行エラーが発生しなくなる。
あとがき
Retrofit 2.9.0 を 2.11.0 にアップデートすると 「手順2: Retrofit2 の ProGuard ルールを適用する」 の手順が不要になる。