Android 開発の「全部覚えていられない」問題
Kotlin、Jetpack Compose、Hilt、Room、Retrofit、Coroutines。Android 開発で「最近のベストプラクティス」とされるツールセットは2〜3年ごとに大きく入れ替わります。私が個人開発で Android アプリを出し始めた頃と現在では、推奨される構成が完全に別物になっており、毎回ドキュメントを当たり直すことに時間を奪われてきました。Kotlin の言語仕様、Jetpack ライブラリ群の急速な進化、多様なデバイス対応、そして Google Play のポリシー変更——これらすべてをひとりの開発者が把握し続けるのは容易ではありません。
Claude Code は、こうした複雑性を「コードで対話する」ことで大幅に緩和してくれます。単なるコード補完ツールではなく、アーキテクチャの相談相手、バグの原因分析パートナー、そしてリリース自動化のオーケストレーターとして機能するのです。
以下では、私自身が個人で運営している壁紙系・癒し系の Android アプリ群を Claude Code に組み込みながら整理してきた、Compose × Hilt × Room の本番設計と Google Play 公開までの判断軸を、実際のコード例と運用数値とともに整理していきます。同じように個人または小規模チームで Android を出荷している方が、明日から1つでも持ち帰れる粒度を意識しました。
対象読者
Kotlin の基礎知識があり、本番品質のアプリ開発を目指している方
Claude Code を Android 開発に組み込みたいと考えている方
Flutter や React Native から Kotlin ネイティブへの移行を検討している方
リリース自動化で開発サイクルを短縮したい方
Step 1:プロジェクト初期設定と CLAUDE.md の活用
プロジェクトの骨格を作る
新規プロジェクト作成後、最初にすべき作業は CLAUDE.md ファイルをプロジェクトルートに配置することです。これにより Claude Code がプロジェクトのコンテキストを正確に把握し、一貫した提案を行えるようになります。
# MyApp — CLAUDE.md
## アーキテクチャ
- パターン: MVVM + Clean Architecture
- DI: Hilt
- DB: Room (SQLite)
- ネットワーク: Retrofit + OkHttp
- 非同期: Kotlin Coroutines + Flow
- UI: Jetpack Compose (Material 3)
## パッケージ構成
- app/ — Application クラス、DI モジュール
- feature/{機能名}/ — 各機能の UI / ViewModel
- domain/ — UseCase、Repository インターフェース
- data/ — Repository 実装、Room DAO、API クライアント
- core/ — 共通ユーティリティ、拡張関数
## コーディングルール
- sealed class でエラー処理を統一(Result<T, AppError>)
- ViewModel は UI State を StateFlow で公開
- テスト: JUnit5 + MockK、カバレッジ 80% 以上
- Compose プレビュー: @PreviewParameter で実データに近いサンプルを用意する
## 禁止事項
- Activity/Fragment に直接ビジネスロジックを書かない
- GlobalScope は使用禁止
- Hardcoded String はすべて strings.xml に移動する
この CLAUDE.md があることで、Claude Code は「このプロジェクトは Hilt を使っている」「GlobalScope は使ってはいけない」といったコンテキストを毎回の会話で保持してくれます。
ビルドスクリプトの整理
複数モジュール構成のプロジェクトでは build.gradle.kts の管理が煩雑になりがちです。Claude Code に以下のように依頼するだけで、バージョンカタログ(libs.versions.toml)への移行を半自動化できます。
/プロジェクト全体の build.gradle.kts を確認して、
libs.versions.toml を使った バージョンカタログ形式に移行してください。
Kotlin 2.0 / AGP 8.9 ベースで最新の依存関係に更新すること。
Claude Code はファイルを読み込み、現在のバージョン一覧を抽出し、libs.versions.toml の雛形と各 build.gradle.kts の修正差分を一括で提案してくれます。
Step 2:Jetpack Compose × MVVM の実装パターン
UI State の設計
Compose ベースのアプリで最も重要なのが UI State の設計です。以下のパターンを Claude Code に伝えることで、一貫したコード生成が得られます。
// domain/state/ProductListState.kt
sealed class ProductListState {
data object Loading : ProductListState ()
data class Success (
val products: List < Product >,
val isRefreshing: Boolean = false
) : ProductListState ()
data class Error ( val message: String , val retryable: Boolean ) : ProductListState ()
}
// feature/products/ProductListViewModel.kt
@HiltViewModel
class ProductListViewModel @Inject constructor (
private val getProductsUseCase: GetProductsUseCase
) : ViewModel () {
private val _uiState = MutableStateFlow < ProductListState >(ProductListState.Loading)
val uiState: StateFlow < ProductListState > = _uiState. asStateFlow ()
init {
loadProducts ()
}
fun loadProducts (forceRefresh: Boolean = false ) {
viewModelScope. launch {
if ( ! forceRefresh) _uiState. value = ProductListState.Loading
else {
// Pull-to-Refresh の場合は Success 状態を維持しつつ isRefreshing を true に
(_uiState. value as ? ProductListState.Success)?. let {
_uiState. value = it. copy (isRefreshing = true )
}
}
getProductsUseCase ()
. onSuccess { products ->
_uiState. value = ProductListState. Success (products)
}
. onFailure { error ->
_uiState. value = ProductListState. Error (
message = error.localizedMessage ?: "不明なエラー" ,
retryable = error is NetworkException
)
}
}
}
}
Claude Code にこのパターンを見せた上で「同様の構成で CartViewModel を作ってください」と依頼すると、Cart 特有のロジック(数量変更、合計金額計算)を自動的に盛り込んだコードが生成されます。
Compose UI の実装
// feature/products/ProductListScreen.kt
@Composable
fun ProductListScreen (
viewModel: ProductListViewModel = hiltViewModel (),
onProductClick: ( Product ) -> Unit
) {
val uiState by viewModel.uiState. collectAsStateWithLifecycle ()
when ( val state = uiState) {
is ProductListState.Loading -> LoadingContent ()
is ProductListState.Success -> {
ProductListContent (
products = state.products,
isRefreshing = state.isRefreshing,
onRefresh = { viewModel. loadProducts (forceRefresh = true ) },
onProductClick = onProductClick
)
}
is ProductListState.Error -> ErrorContent (
message = state.message,
onRetry = if (state.retryable) {
{ viewModel. loadProducts () }
} else null
)
}
}
@Composable
private fun ProductListContent (
products: List < Product >,
isRefreshing: Boolean ,
onRefresh: () -> Unit,
onProductClick: ( Product ) -> Unit
) {
val pullRefreshState = rememberPullRefreshState (
refreshing = isRefreshing,
onRefresh = onRefresh
)
Box (modifier = Modifier. pullRefresh (pullRefreshState)) {
LazyColumn (
contentPadding = PaddingValues ( 16 .dp),
verticalArrangement = Arrangement. spacedBy ( 12 .dp)
) {
items (products, key = { it.id }) { product ->
ProductCard (
product = product,
onClick = { onProductClick (product) },
modifier = Modifier. animateItem () // Compose 1.7+ API
)
}
}
PullRefreshIndicator (
refreshing = isRefreshing,
state = pullRefreshState,
modifier = Modifier. align (Alignment.TopCenter)
)
}
}
Claude Code への依頼コツとして、「animateItem() のように最新 API を使って」「プレビュー用のサンプルデータも生成して」と添えると、プロダクションで使えるコードが一発で得られます。
Step 3:Hilt × Room × Retrofit の本番グレード設定
依存性注入モジュールの構成
// app/di/NetworkModule.kt
@Module
@InstallIn (SingletonComponent:: class )
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient (
authInterceptor: AuthInterceptor ,
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient = OkHttpClient. Builder ()
. addInterceptor (authInterceptor)
. addInterceptor (loggingInterceptor)
. connectTimeout ( 30 , TimeUnit.SECONDS)
. readTimeout ( 30 , TimeUnit.SECONDS)
. build ()
@Provides
@Singleton
fun provideRetrofit (okHttpClient: OkHttpClient ): Retrofit =
Retrofit. Builder ()
. baseUrl (BuildConfig.API_BASE_URL)
. client (okHttpClient)
. addConverterFactory (
Json { ignoreUnknownKeys = true }. asConverterFactory (
"application/json" . toMediaType ()
)
)
. build ()
@Provides
@Singleton
fun provideProductApi (retrofit: Retrofit ): ProductApi =
retrofit. create (ProductApi:: class .java)
}
// app/di/DatabaseModule.kt
@Module
@InstallIn (SingletonComponent:: class )
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase ( @ApplicationContext context: Context ): AppDatabase =
Room. databaseBuilder (context, AppDatabase:: class .java, "app_database" )
. addMigrations (MIGRATION_1_2, MIGRATION_2_3)
. build ()
@Provides
fun provideProductDao (database: AppDatabase ): ProductDao =
database. productDao ()
}
Claude Code に「マイグレーション MIGRATION_2_3 を作って。products テーブルに category_id (INTEGER NOT NULL DEFAULT 0) カラムを追加する」と依頼すると、正しい SQL とテストコードが同時に生成されます。
Offline-First アーキテクチャの実装
// data/repository/ProductRepositoryImpl.kt
class ProductRepositoryImpl @Inject constructor (
private val productApi: ProductApi ,
private val productDao: ProductDao ,
private val networkMonitor: NetworkMonitor
) : ProductRepository {
override fun getProducts (): Flow < Result < List < Product >>> = flow {
// キャッシュを先にエミット(楽観的 UI 更新)
val cachedProducts = productDao. getAllProducts (). first ()
if (cachedProducts. isNotEmpty ()) {
emit (Result. success (cachedProducts. map { it. toDomain () }))
}
// ネットワーク接続時のみ API 取得
if (networkMonitor.isConnected) {
runCatching { productApi. getProducts () }
. onSuccess { response ->
productDao. upsertProducts (response. map { it. toEntity () })
emit (Result. success (response. map { it. toDomain () }))
}
. onFailure { error ->
if (cachedProducts. isEmpty ()) {
emit (Result. failure (error))
}
// キャッシュがある場合はエラーをサイレントに処理
}
}
}. catch { emit (Result. failure (it)) }
}
このパターンを Claude Code に説明した上で「同様の Offline-First パターンで OrderRepository を実装して」と依頼すると、Order エンティティに合わせた実装が素早く得られます。
Step 4:テスト自動化と品質保証
ViewModel のユニットテスト
// feature/products/ProductListViewModelTest.kt
@ExtendWith (CoroutinesTestExtension:: class )
class ProductListViewModelTest {
@MockK private lateinit var getProductsUseCase: GetProductsUseCase
private lateinit var viewModel: ProductListViewModel
@BeforeEach
fun setUp () {
MockKAnnotations. init ( this )
viewModel = ProductListViewModel (getProductsUseCase)
}
@Test
fun `初期状態はローディング` () = runTest {
// ローディング状態がすぐにエミットされる前にテスト
coEvery { getProductsUseCase () } coAnswers {
delay ( 1000 ) // API 遅延をシミュレート
Result. success ( emptyList ())
}
val states = mutableListOf < ProductListState >()
val job = launch { viewModel.uiState. collect { states. add (it) } }
assertTrue (states. first () is ProductListState.Loading)
job. cancel ()
}
@Test
fun `成功時は商品リストが表示される` () = runTest {
val products = listOf (
Product (id = "1" , name = "商品A" , price = 1000 ),
Product (id = "2" , name = "商品B" , price = 2000 )
)
coEvery { getProductsUseCase () } returns Result. success (products)
val finalState = viewModel.uiState
. filter { it is ProductListState.Success }
. first ()
assertThat (finalState). isInstanceOf (ProductListState.Success:: class .java)
assertThat ((finalState as ProductListState.Success).products). isEqualTo (products)
}
}
Claude Code への依頼パターン: 「ProductListViewModel のテストを MockK で書いてください。エラーケース(ネットワークエラー / 空リスト)も含めて」と伝えると、包括的なテストスイートが一度に生成されます。
UI テスト(Compose Testing)
// feature/products/ProductListScreenTest.kt
@RunWith (AndroidJUnit4:: class )
class ProductListScreenTest {
@get : Rule
val composeTestRule = createComposeRule ()
@Test
fun 商品一覧が正常表示される () {
val products = createSampleProducts (count = 5 )
composeTestRule. setContent {
MaterialTheme {
ProductListContent (
products = products,
isRefreshing = false ,
onRefresh = {},
onProductClick = {}
)
}
}
composeTestRule
. onNodeWithText ( "商品A" )
. assertIsDisplayed ()
composeTestRule
. onAllNodesWithTag ( "product_card" )
. assertCountEquals ( 5 )
}
}
Step 5:CI/CD パイプラインの構築
GitHub Actions ワークフロー
# .github/workflows/android_ci.yml
name : Android CI
on :
push :
branches : [ main , develop ]
pull_request :
branches : [ main ]
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Set up JDK 17
uses : actions/setup-java@v4
with :
java-version : '17'
distribution : 'temurin'
- name : Cache Gradle packages
uses : actions/cache@v4
with :
path : |
~/.gradle/caches
~/.gradle/wrapper
key : ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name : Run unit tests
run : ./gradlew testDebugUnitTest --stacktrace
- name : Upload test results
if : always()
uses : actions/upload-artifact@v4
with :
name : test-results
path : '**/build/reports/tests/'
build_release :
runs-on : ubuntu-latest
needs : test
if : github.ref == 'refs/heads/main'
steps :
- uses : actions/checkout@v4
- name : Set up JDK 17
uses : actions/setup-java@v4
with :
java-version : '17'
distribution : 'temurin'
- name : Build release APK
env :
KEYSTORE_FILE : ${{ secrets.KEYSTORE_FILE }}
KEYSTORE_PASSWORD : ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS : ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD : ${{ secrets.KEY_PASSWORD }}
run : |
echo "$KEYSTORE_FILE" | base64 -d > app/keystore.jks
./gradlew bundleRelease \
-Pandroid.injected.signing.store.file=keystore.jks \
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
- name : Upload AAB to Google Play
uses : r0adkll/upload-google-play@v1
with :
serviceAccountJsonPlainText : ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName : com.example.myapp
releaseFiles : app/build/outputs/bundle/release/*.aab
track : internal
status : completed
このワークフローを Claude Code に「レビューして、改善点があれば提案して」と依頼すると、キャッシュ戦略の最適化や並列実行の提案など、実運用で役立つ改善案が得られます。
Claude Code による自動化スクリプト
さらに一歩進めて、Claude Code の Hooks 機能と組み合わせると、コミット時に自動でテストを実行し、PR 作成を補助するワークフローが構築できます。
// .claude/hooks/post-commit.json
{
"hooks" : {
"PostToolUse" : [{
"matcher" : "Write|Edit" ,
"hooks" : [{
"type" : "command" ,
"command" : "if [[ \" $CLAUDE_TOOL_OUTPUT \" == *'.kt'* ]]; then ./gradlew testDebugUnitTest 2>&1 | tail -20; fi"
}]
}]
}
}
Claude Code で Kotlin ファイルを編集するたびに、対応するユニットテストが自動実行されます。テストが失敗すると Claude Code はその出力を受け取り、次の提案でエラーを修正するコードを提案してくれます。
詳細な Hooks 設定については Claude Code Hooks 自動化テクニック も参考にしてください。
Step 6:Google Play ストア公開と ASO 最適化
リリースビルドの設定
// app/build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles (
getDefaultProguardFile ( "proguard-android-optimize.txt" ),
"proguard-rules.pro"
)
signingConfig = signingConfigs. getByName ( "release" )
}
}
flavorDimensions += "environment"
productFlavors {
create ( "staging" ) {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
buildConfigField ( "String" , "API_BASE_URL" , " \" https://staging-api.example.com/ \" " )
}
create ( "production" ) {
dimension = "environment"
buildConfigField ( "String" , "API_BASE_URL" , " \" https://api.example.com/ \" " )
}
}
}
Claude Code による ASO 最適化
Google Play のメタデータ(タイトル・説明文・リリースノート)の最適化も Claude Code が得意とする領域です。
以下の情報をもとに、Google Play ストアの最適化されたメタデータを
日本語と英語で生成してください。
アプリ名: TaskFlow
ジャンル: タスク管理 / 生産性
主要機能:
- Kanban ボード
- 自然言語でのタスク追加(AI解析)
- チーム共有・コラボ
- 通知リマインダー
競合アプリ: Todoist, TickTick, Notion
ターゲット: ビジネスパーソン 25〜45歳
出力形式:
- タイトル(30文字以内)
- 短い説明(80文字以内)
- 詳細説明(4,000文字以内、SEOキーワード含む)
- リリースノート(500文字以内)
Claude Code が生成するテキストをそのまま使えることも多いですが、A/Bテストを通じて最適化していくのがベストプラクティスです。
Google Play 公開チェックリスト
Claude Code に以下のプロンプトを送ると、リリース前の確認作業を体系的に整理してくれます。
Google Play への本番リリース前チェックリストを作成してください。
技術的な確認項目とビジネス的な確認項目を分けてください。
ProGuard / R8 の難読化確認、デバッグログの除去、アナリティクス設定も含めること。
生成されたチェックリストの主要項目:
ProGuard / R8 が有効になっており、重要なクラスが保護されているか
デバッグログ(Log.d, Timber.d)が本番ビルドで除去されているか
API キーが BuildConfig または Secrets Manager で管理されており、コードにハードコードされていないか
ネットワーク通信がすべて HTTPS であるか(Network Security Config)
ターゲット API レベルが最新(現在は API 35 以上が必須)
64-bit アーキテクチャ(arm64-v8a)対応済みか
アプリサイズが最適化されているか(AAB 形式で Dynamic Delivery)
Step 7:パフォーマンス最適化と本番モニタリング
Compose パフォーマンスの最適化
Compose アプリで最も多いパフォーマンス問題は「不必要な再コンポーズ」です。Claude Code を使ったデバッグアプローチ:
// コンポーズ回数を視覚化するデバッグモード
@Composable
fun DebugRecompositionHighlight (
enabled: Boolean = BuildConfig.DEBUG,
content: @Composable () -> Unit
) {
if (enabled) {
val recompositionCount = remember { mutableIntStateOf ( 0 ) }
val color by animateColorAsState (
targetValue = if (recompositionCount.intValue % 2 == 0 ) {
Color.Transparent
} else {
Color.Red. copy (alpha = 0.1f )
},
label = "recompose_highlight"
)
LaunchedEffect (recompositionCount.intValue) {
// 再コンポーズ時に色が変化する視覚的インジケータ
}
recompositionCount.intValue ++
Box (modifier = Modifier. background (color)) { content () }
} else {
content ()
}
}
Claude Code への依頼: 「このスクリーンで再コンポーズが多発しています。ViewModel の StateFlow と Compose の collectAsStateWithLifecycle の使い方を確認して、最適化案を提案してください」と伝えると、remember・derivedStateOf・key() の活用方法を含む具体的な改善提案が得られます。
Firebase Crashlytics + Performance Monitoring
// app/src/main/kotlin/com/example/myapp/MyApplication.kt
@HiltAndroidApp
class MyApplication : Application () {
override fun onCreate () {
super . onCreate ()
// Crashlytics 初期化
if ( ! BuildConfig.DEBUG) {
FirebaseCrashlytics. getInstance (). apply {
setCrashlyticsCollectionEnabled ( true )
}
}
// Performance Monitoring のカスタムトレース
setupPerformanceMonitoring ()
}
private fun setupPerformanceMonitoring () {
FirebasePerformance. getInstance ().isPerformanceCollectionEnabled = ! BuildConfig.DEBUG
}
}
// feature/products/ProductListViewModel.kt への追加
fun loadProducts () {
val trace = Firebase.performance. newTrace ( "load_products" )
trace. start ()
viewModelScope. launch {
try {
// ... 既存のロード処理 ...
trace. putAttribute ( "product_count" , products.size. toString ())
trace. stop ()
} catch (e: Exception ) {
trace. putAttribute ( "error" , e.javaClass.simpleName)
trace. stop ()
FirebaseCrashlytics. getInstance (). recordException (e)
}
}
}
Step 8:Claude Code の効果的な使い方まとめ
5つの実践的なプロンプトパターン
Android 開発で Claude Code を最大限に活用するためのプロンプトパターンをまとめます。
パターン1:コンテキストを先に与える
/read app/src/main/kotlin/com/example/myapp/feature/products/
このディレクトリの構成を確認した上で、CartFeature を同じパターンで実装してください。
パターン2:エラーメッセージをそのまま貼る
以下のビルドエラーを修正してください:
[エラーメッセージをそのままペースト]
パターン3:既存コードの改善依頼
/read app/src/main/kotlin/com/example/myapp/data/repository/ProductRepositoryImpl.kt
このファイルをレビューして、Kotlin のベストプラクティスに沿った改善案を提示してください。
特に Coroutines の使い方と例外処理に注目してください。
パターン4:テストファースト開発
ProductRepositoryImpl の getProducts() のユニットテストを書いてください。
MockK を使用し、成功・ネットワークエラー・キャッシュ返却の3ケースをカバーすること。
パターン5:リファクタリング補助
このクラスは 500 行を超えています。責務を分離してください。
単一責任の原則(SRP)に従い、分割後のクラス構成を提案してから実装してください。
Git ワークフローとの統合
Claude Code と Claude Code Git ワークフロー自動化 を組み合わせると、ブランチ管理からコミットメッセージの自動生成まで効率化できます。また、App Store Connect リリース自動化 の手法を Google Play にも応用することで、リリースサイクルをさらに短縮できます。
開発中につまずきやすい2点
Kotlin Multiplatform(KMP)プロジェクトでの expect / actual 実装
KMP プロジェクトでは expect / actual の実装パターンが複雑になりがちです。Claude Code に「Android と iOS の actual 実装を両方作って」と依頼するとひな形は出ますが、KMP 特有の Gradle 設定やネイティブクラッシュのデバッグはまだ人間のレビューが必要です。CLAUDE.md に KMP の構成(共通モジュール・プラットフォーム別モジュールの責務分担)を詳細に記載しておくと、出力精度が大きく変わります。
ProGuard / R8 の難読化後クラッシュ
「このライブラリを使っているが、難読化後にクラッシュします。クラッシュログを基に適切な keep ルールを提案して」というプロンプトが有効です。Claude Code はクラッシュログから保護が必要なクラスを特定し、proguard-rules.pro に追加すべきルールを生成してくれます。ただし、最終的な動作確認は必ず実機テストで行ってください。
公式ドキュメントには書かれていない Android × Claude Code の運用知見
ここからが、個人で複数の Android アプリを長く回している中で、Claude Code に任せる前提で組み直して初めて見えてきた落とし穴の話です。Android Developers のドキュメント、Compose 公式サンプル、Kotlin リファレンスのどこにも明示されていない領域なので、本番に出してから初めて気づくと修正コストが跳ね上がります。
1. Compose の recomposition 暴走と @Stable / @Immutable の付与基準
Compose の最大の落とし穴は「画面遷移しないのに recomposition が秒間 200 回走る」現象です。私の壁紙アプリでログを取ったところ、一覧画面で Image を含む LazyVerticalGrid 配下のアイテムが、スクロール 1px ごとに recompose を呼んでしまい、ミドルレンジ Android で平均フレームレートが 60fps → 38fps まで落ちました。
原因は ViewModel から流す ProductUiState を data class のまま渡していたためで、Compose コンパイラが Stable と判定できず、毎フレームで等価判定をスキップしていました。
// ❌ Bad: パラメータが多くて Compose コンパイラが Stable 判定を諦める
data class ProductUiState (
val id: String ,
val title: String ,
val tags: List < String >,
val downloads: Int ,
val previewUrls: List < String >,
val isFavorite: Boolean ,
)
// ✅ Good: @Immutable を明示し、List は ImmutableList に置換
@Immutable
data class ProductUiState (
val id: String ,
val title: String ,
val tags: ImmutableList < String >, // kotlinx.collections.immutable
val downloads: Int ,
val previewUrls: ImmutableList < String >,
val isFavorite: Boolean ,
)
Claude Code には「@Immutable を付与すべき箇所と、ImmutableList に置換すべき List を、composable 関数のシグネチャを起点に列挙して」とプロンプトすると、ファイル単位でまとめて出してくれます。実機計測でフレームレートが安定したのは、画面ごとに @Immutable を 4〜7 個追加した時点でした。
2. Coroutines の例外がアプリを落とす ── CoroutineExceptionHandler の効きどころ
viewModelScope.launch { ... } の中で握りつぶさずに throw すると、SupervisorJob を持っているはずなのにプロセス全体が落ちることがあります。原因は子コルーチンが async だった場合の await() 呼び出し漏れと、launch ブロック内で Dispatchers.IO に切り替えた後の runCatching 漏れです。
Crashlytics で月 1,200 件出ていた UndeliverableException 系のクラッシュは、以下の 3 段防御を入れたところ 月 40 件まで落ちました(97% 減)。
// 1. グローバルハンドラ(最後の砦)
class App : Application () {
override fun onCreate () {
super . onCreate ()
Thread. setDefaultUncaughtExceptionHandler { _, throwable ->
FirebaseCrashlytics. getInstance (). recordException (throwable)
}
}
}
// 2. ViewModel 側で握る
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
FirebaseCrashlytics. getInstance (). recordException (throwable)
_uiState. update { it. copy (error = throwable. toUiError ()) }
}
fun loadProducts () {
viewModelScope. launch (exceptionHandler) {
runCatching { repository. getProducts () }
. onSuccess { products -> _uiState. update { it. copy (products = products) } }
. onFailure { e -> FirebaseCrashlytics. getInstance (). recordException (e) }
}
}
// 3. RxJava 互換層が残っている場合の Plugin(旧モジュール混在で必須)
RxJavaPlugins. setErrorHandler { e ->
if (e is UndeliverableException) {
FirebaseCrashlytics. getInstance (). recordException (e.cause ?: e)
return @setErrorHandler
}
Thread. currentThread ().uncaughtExceptionHandler?. uncaughtException (Thread. currentThread (), e)
}
Claude Code には「この ViewModel の launch 呼び出しから、CoroutineExceptionHandler が付いていない箇所を全列挙して」と頼むと、grep ベースで漏れを潰してくれます。
3. Room の Migration を Claude Code に書かせる時の罠
「カラムを追加して」と頼むと Claude Code は ALTER TABLE の SQL を出してくれますが、fallbackToDestructiveMigration() のままだと本番ユーザーのデータが消えます。私のアプリでも、お気に入り壁紙が消えたという報告が立て続けに来て初めて気付きました。
実運用ルールは 4 つです。
fallbackToDestructiveMigration() は開発時のみ。 リリース build には絶対に残さない(lint ルールで弾く)
Migration は MIGRATION_X_Y を別ファイルに切り出す (PR で diff が見やすくなる)
exportSchema = true で JSON スキーマを VCS にコミット (Claude Code に旧スキーマを読ませて差分 SQL を生成させるため)
MigrationTestHelper で全 Migration を CI で実行 (本番 DB を再現してテストしないと Foreign Key 制約で落ちる)
「schemas/com.example.MyDatabase/3.json と 4.json の差分を読み、MIGRATION_3_4 を書き、MigrationTestHelper テストも生成して」というプロンプトで、3 つを同時に出してもらえます。
4. ProGuard / R8 の難読化後クラッシュは「最小 keep ルール」で
「全部 keep」は Code shrinking が機能しなくなるので APK サイズが膨らみます。私のアプリでは、最小 keep ルールに整理した結果、APK サイズが 18.4 MB → 12.1 MB(-34%)まで縮みました。Google Play の「アプリサイズ警告」も解消されました。
Claude Code には Crashlytics の「Top crashes (last 7 days)」をそのまま貼って「このクラッシュログから、最小限の -keep ルールを proguard-rules.pro 用に生成して。広範囲な -keep class ** は禁止」と頼みます。GSON のような reflection ベースのライブラリは、データクラスのみ keep(メソッドは keep しない)にするのがコツです。
# ✅ Good: データクラスのフィールドだけ守る
-keepclassmembers class com.example.myapp.data.dto.** {
<fields>;
}
# ❌ Bad: メソッド含めて全部 keep(APK サイズが膨らむ)
-keep class com.example.myapp.data.dto.** { *; }
5. WorkManager の OS バージョン別挙動と OEM doze
WorkManager で定期同期を組むと、Android 公式仕様では 15 分間隔で動くはずなのに、Xiaomi/OPPO/Vivo 端末では 6 時間以上動かないことがあります。各 OEM が独自の電池最適化(doze ライク機構)を実装しているためです。
Claude Code には以下のように頼んで、OEM 別フォールバックを自動生成してもらいました。
WorkManager の Periodic Work を 15 分間隔で実行したいが、Xiaomi/OPPO/Vivo の独自電池最適化に
当たって動かないユーザーが 22% いる。Build.MANUFACTURER で分岐して、該当 OEM では
「自動起動許可」「電池最適化対象外」設定画面への誘導 Intent を生成するヘルパーを書いて。
完璧な解はありませんが、影響を受けるユーザーへの導線設計を Compose の BottomSheet で出すパターンに集約できました(実装後、定期同期成功率が 62% → 87% まで改善)。
個人で Android を出荷し続ける視点から見える Claude Code × Android の設計判断
公式の使い方を超えて、AdMob を回しながら個人で Android を出荷し続ける視点で見たときに、Claude Code を「どこに効かせるか」の判断軸を共有します。私自身が壁紙アプリ・癒し系アプリで実際に回してきた感覚値です。
共通化率と AdMob eCPM の関係
Compose × Hilt × Room のスタックに統一すると、複数アプリ間のコード共通化率が体感で 70〜80% まで上がります。私の場合、壁紙アプリ群 8 本のうち 6 本がほぼ同じ feature モジュールを共有しており、新作を 1 本追加する工数が 3〜4 日(昔の半分以下)に圧縮されました。
ただし、共通化を進めるほどビルド時間と依存解決の複雑性は増えます。Claude Code には「この feature モジュールの依存関係を可視化して、循環依存と未使用依存を列挙して」と頼むと、Gradle の :app:dependencies 出力を整理してくれます。月 1 回これを回しておくと、ビルド時間の劣化が見えるようになります。
AdMob 視点では、Compose で構築した UI の方が広告のレンダリング遅延(PaintInteract)が短く、結果として AdMob eCPM が +12% 程度高い感触があります(厳密には UI 速度よりセッション継続率の効果と思います)。
"実装ノートを Claude に書かせる" 戦略
私のリポジトリでは、新規 feature モジュールを実装する前に必ず Claude Code に「この機能の実装ノートを Markdown で書いて。要件・データフロー・状態遷移・例外ケースの 4 セクション構成」と頼みます。これを docs/features/<name>.md に置いてから実装に入ると、後から「なぜこの設計にしたか」が残るため、自分自身が 3 ヶ月後に同じファイルを触る時にも迷いません。
実装ノートが残っているとリファクタリングを Claude Code に依頼する時の精度も上がります。プロンプトに「docs/features/cart.md を踏まえてリファクタリングしてください」と添えるだけで、設計意図を破壊しない範囲での提案に絞り込まれます。
Compose × XML 混在モジュールの優先順位
10 年以上運用しているアプリでは、レガシーな XML レイアウトと Compose が混在することがほとんどです。「全部 Compose に書き換える」のは投資対効果が悪く、私は以下の 3 軸で優先順位を決めています。
広告表示頻度の高い画面から Compose 化 (recomposition 制御で eCPM 改善が見える)
状態が複雑な画面から Compose 化 (XML の findViewById 沼を抜けるだけで、保守工数が半減する)
静的なヘルプ・利用規約系は最後 (数年触らないため)
Claude Code に「この XML レイアウトを Compose に書き換えて。AndroidViewBinding でラップして既存 ViewModel との接続を維持する案も併記して」と頼むと、段階的移行のパターンが出てきます。一度に全部書き換えない判断が、長期運用するアプリでは重要です。
インストール直後 60 秒の体験を Claude Code に設計させる
Google Play からインストールしてから 60 秒以内に「次のアクション」を取らせる導線が、リテンションを大きく左右します。私のアプリでは Day-1 リテンションが 18% → 31% まで改善した変更のほとんどが、最初の 60 秒の体験リファクタリングでした。
Claude Code に「MainActivity の onCreate から最初の Composable 描画までの処理を時系列で書き出して。各処理の想定時間(ミリ秒)と、削れるものを提案して」と頼むと、初期化処理の重複や順序の最適化候補が出てきます。Hilt の eager initialization、Room の init ブロックでの I/O、Firebase Remote Config の同期取得などが典型的なボトルネックです。
Google Play 審査・本番リリース直前の実装チェックリスト 14 項目
毎回これを通してから AAB を Google Play Console にアップロードしています。1 項目でも飛ばすと「ポリシー違反通知」「審査差し戻し」「ANR 急増」のどれかを必ず食らうので、Claude Code に各項目を順に検証してもらうフローを作っておくと安心です。
minSdk と targetSdk のポリシー期限を確認 — Google Play は毎年 8 月頃に targetSdk の最低基準を引き上げます。期限から逆算して 2 ヶ月前にはアップ完了が目安
64-bit ABI を含む — arm64-v8a と x86_64 を必ずビルド構成に入れる。ネイティブライブラリを使う場合は CMake の abiFilters を要確認
App Bundle (.aab) でアップロード — APK 直接アップロードは新規ストア提出で不可
ProGuard / R8 が有効 — release build で minifyEnabled true を必ず確認
Crashlytics と Performance Monitoring が release で動く — firebase-perf の Gradle plugin が release variant で適用されているか
Data Safety フォームの記入内容と SDK の実装が一致 — AdMob・Firebase Analytics・課金ライブラリが収集するデータを Console に全て申告(虚偽申告でアプリ停止される)
広告 ID 許可 — Android 13 以降は com.google.android.gms.permission.AD_ID を明示的に AndroidManifest に追加(targetSdk 33+ で必須)
ストア掲載スクリーンショットが最新 UI と一致 — 古いスクショのまま公開すると審査ではないが、ユーザー評価が下がる
アプリ内課金(IAP)の Pricing Tier が国別に適切 — JP / US / 新興市場で 1:1 の換算は失敗する。Claude Code に「日本円 ¥250 を主要 30 通貨に Purchasing Power Parity ベースで換算して」と頼むと一覧が出る
<queries> 要素の宣言 — Android 11+ で他アプリを照会する場合は AndroidManifest に必須
エッジトゥエッジ対応 — Android 15+ ではデフォルトでエッジトゥエッジが強制されるため、WindowCompat.setDecorFitsSystemWindows(window, false) と Modifier.windowInsetsPadding の整理が必要
ANR 監視 — Play Console の Vitals で「ユーザーが感じる ANR」が 0.47% 以下に収まっているか(超えると検索結果での順位が落ちる)
オフライン起動できる — 機内モードでアプリを起動して、最低限のキャッシュ画面が出るか
アンインストール経路の動線 — ユーザーが「設定」→「アプリ」から削除できることは大前提だが、アプリ内に「データ削除」リクエストの導線があるか(GDPR / 個人情報保護法対応)
Claude Code には「AndroidManifest.xml / build.gradle.kts / MainActivity.kt を読んで、上記 14 項目を順に検証して、足りないものを項目番号付きで指摘して」と一気にお願いします。私のアプリでは平均 2〜3 項目が毎回引っかかります。
次の一手 — 今日から1つだけやるとしたら
ここまでに書いた全部を一気にやろうとすると確実に手が止まるので、もし今日 1 つだけやるとしたら 「Crashlytics の Top 5 crashes を Claude Code に貼って、最小 keep ルールと CoroutineExceptionHandler の不足箇所だけを潰す」 をおすすめします。私のアプリでは、これだけで月のクラッシュ数が 1,200 → 350 まで落ちました(-71%)。
12 年・累計 5,000 万 DL という数字も、振り返ると「すごい何か」をやったというより、毎週この種の小さな観測 → 1 箇所修正を積み上げてきただけです。Claude Code が入ったことで、観測から修正までのループが 5 分単位で回せるようになりました。観測の手数が増えたぶん、判断する側(人間)の責任は重くなります。CLAUDE.md にプロジェクトの「外せない原則」を書き残しておくこと、それを Claude Code との対話で頻繁に参照させること ── この 2 つだけは、AI に任せきりにせず手で守り続けたいと考えています。
Claude Code に CLAUDE.md を書く際の設計判断にも応用できる内容です。