Claude Code for Android — Kotlin, Jetpack Compose & Gradle
Android development with Claude Code requires careful CLAUDE.md configuration because the Android ecosystem has multiple layers: Gradle build system, Kotlin 2.0, Jetpack Compose UI toolkit, Architecture Components (ViewModel, Room, Navigation), and dependency injection with Hilt. This guide covers the complete CLAUDE.md template for Android projects, automated Gradle test hooks, Compose workflow patterns, Room database generation, Retrofit networking, and Hilt DI scaffolding.
Android CLAUDE.md Template
# Project: [Your Android App Name]
## Platform
- Android minSdk 26 / targetSdk 35 / compileSdk 35
- Kotlin 2.0 with K2 compiler
- Compose BOM: 2026.xx.xx (Compose 1.8+)
- AGP: 8.7+
## Key commands
```bash
./gradlew assembleDebug # build debug APK
./gradlew testDebugUnitTest # run unit tests (JVM, fast)
./gradlew lintDebug # lint checks
./gradlew testDebugUnitTest --tests "*.ViewModelTest" # single test class
./gradlew connectedAndroidTest # instrumented tests (needs emulator)
```
## Architecture: MVVM + Clean Architecture
- presentation/ — Composables + ViewModels (one per screen)
- domain/ — UseCases + domain models + Repository interfaces
- data/ — Repository implementations, Room DAOs/entities, Retrofit API services
- di/ — Hilt modules (NetworkModule, DatabaseModule, RepositoryModule)
## Stack
- UI: Jetpack Compose 1.8 (no XML layouts)
- DI: Hilt 2.x (@HiltViewModel, @AndroidEntryPoint)
- Navigation: Compose Navigation 2.8+ (type-safe routes with @Serializable data classes)
- Database: Room 2.7 (suspend DAOs, Flow queries)
- Network: Retrofit 2.11 + OkHttp 5 + kotlinx.serialization
- Async: Kotlin Coroutines + Flow (no RxJava)
- Images: Coil 3 (AsyncImage Composable)
- Testing: JUnit 5 + MockK + Turbine (Flow testing)
## Conventions
- ViewModel: holds UiState (data class) in StateFlow; expose events via SharedFlow
- Composables: stateless; receive UiState + callbacks as params; no ViewModel access inside
- Error handling: Result<T> in domain layer; UiState.error field in presentation
- Coroutines: viewModelScope for ViewModel; IO dispatcher injected (testable)
- Never expose Room entities to ViewModel — map to domain models in Repository
- All Composables get @Preview annotations (light + dark theme)
Automated Gradle Test Hooks
// .claude/settings.json
{
"allowedTools": ["Edit", "Write", "Bash", "Read", "Glob", "Grep"],
"hooks": [
{
"event": "PostToolUse",
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "cd $PROJECT_ROOT && ./gradlew testDebugUnitTest --quiet 2>&1 | grep -E '(FAILED|ERROR|BUILD FAILED|tests were|passed)' | tail -15"
}]
}
]
}
Use
./gradlew testDebugUnitTest --tests "com.example.app.feature.*" to run tests for a specific feature package. JVM unit tests run in seconds; save instrumented tests for CI.Jetpack Compose Workflow Patterns
| Pattern | CLAUDE.md instruction |
|---|---|
| UiState model | "ViewModel exposes a single UiState data class via StateFlow; never multiple separate StateFlows for related state" |
| Event handling | "User events as sealed class UiEvent; ViewModel processes via handleEvent(event: UiEvent)" |
| Side effects | "One-shot effects (navigation, toasts) as Channel<UiEffect>; collect in Composable with LaunchedEffect" |
| Loading states | "UiState has isLoading: Boolean field; show CircularProgressIndicator when true overlaying content" |
| Error states | "UiState has errorMessage: String? field; show Snackbar via LocalSnackbarHostState; clear on dismiss" |
| Previews | "Every Screen Composable gets @PreviewLightDark @Preview with hardcoded UiState instances" |
New feature screen (Compose + ViewModel)
claude "add a ProductListScreen feature.
ViewModel: ProductListViewModel (@HiltViewModel).
UiState: data class with products: List<Product>, isLoading: Boolean, errorMessage: String?.
On init: load products via GetProductsUseCase (inject via constructor).
Screen Composable: LazyColumn of ProductCard items; swipe-to-refresh; error Snackbar.
Navigation: add ProductListRoute to the nav graph; navigate to ProductDetailScreen on tap.
Add @PreviewLightDark with 3 mock products and with isLoading=true."
Room Database Workflows
# CLAUDE.md addition for Room
## Room conventions
- Entities: data classes in data/local/entity/; @Entity(tableName = "snake_case")
- Primary key: @PrimaryKey val id: String (UUID) or Long (auto-generate)
- DAOs: interface in data/local/dao/; all methods suspend or return Flow
- Database: AppDatabase.kt — @Database(entities, version); provide via DatabaseModule
- Migrations: AutoMigration preferred; manual Migration class for complex changes
- Never use fallbackToDestructiveMigration in production builds
- TypeConverters: convert Instant/LocalDate to Long; List<String> to JSON string
claude "add a NoteEntity and NoteDao for a note-taking feature.
NoteEntity: id (UUID string), title, body, createdAt (Instant as Long), isPinned (Boolean).
NoteDao: insertOrReplace(note), deleteById(id: String), getAll(): Flow<List<NoteEntity>>,
getById(id: String): Flow<NoteEntity?>, getPinned(): Flow<List<NoteEntity>>.
Add TypeConverter for Instant ↔ Long.
Update AppDatabase to include NoteEntity with a version bump migration."
Retrofit Networking
# CLAUDE.md addition for networking
## Networking conventions
- HTTP client: Retrofit 2.11 + OkHttp 5 with interceptors
- Serialization: kotlinx.serialization (@Serializable data classes)
- API service interface: suspend functions returning Response<T> or T directly
- Error handling: NetworkResult sealed class (Success, Error, Loading)
- Base URL + auth interceptor provided via Hilt NetworkModule
- Logging: HttpLoggingInterceptor only in debug builds
- Timeouts: connect=10s, read=30s, write=30s
claude "add a GitHubApiService with Retrofit.
Endpoints: GET /users/{username} → GitHubUser; GET /users/{username}/repos → List<GitHubRepo>.
Define @Serializable data classes for the responses.
Create GitHubRepository that wraps the service, maps to domain User/Repo models.
Handle HTTP 404 as UserNotFoundException; 403 as RateLimitException.
Add unit tests with MockWebServer for both endpoints."
Hilt DI Scaffolding
| Module | Claude Code prompt snippet |
|---|---|
| NetworkModule | "Create NetworkModule @InstallIn(SingletonComponent): provide OkHttpClient with auth interceptor + logging, Retrofit with kotlinx.serialization, and all @ApiService interfaces" |
| DatabaseModule | "Create DatabaseModule @InstallIn(SingletonComponent): provide AppDatabase (context, fallbackToDestructiveMigration=false), all @Dao interfaces via db.noteDao()" |
| RepositoryModule | "Create RepositoryModule @InstallIn(SingletonComponent): @Binds NoteRepositoryImpl to NoteRepository interface" |
| CoroutineModule | "Create CoroutineModule: @Provides @IoDispatcher Dispatcher = Dispatchers.IO; inject into Repositories and UseCases for testability" |
5 Tips for Android + Claude Code
- Paste your
libs.versions.toml(version catalog) into CLAUDE.md. Claude will reference the exact library aliases and versions instead of inventing dependency coordinates that don't match your catalog. - For large Gradle builds, split the PostToolUse hook to run only unit tests:
./gradlew testDebugUnitTestinstead of a full build. This gives sub-30s feedback on logic changes without waiting for the full APK build. - When working on Compose UI, tell Claude your design system: "We use Material 3 with a custom theme in ui/theme/. Colors are in AppTheme.kt. Use MaterialTheme.colorScheme.primary, never hardcode colors." This prevents Claude from mixing Material 2 and Material 3 APIs.
- For navigation, give Claude your full type-safe route definitions: paste your NavRoutes sealed class into CLAUDE.md. Claude will generate navigationDestination() calls with the correct type-safe arguments instead of string-based route navigation.
- Use
./gradlew lintDebugas a secondary PostToolUse hook. Android Lint catches Compose-specific issues (missing content descriptions, hardcoded strings, deprecated API usage) that the Kotlin compiler misses.