Android Kotlin
Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing
MCP get_skill({ skillId: "android-kotlin-skill-d82d1df4" })Use this skill with your agent
Create a free account and connect via MCP
# Android Kotlin Skill
---
## Project Structure
```
project/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── kotlin/com/example/app/
│ │ │ │ ├── data/ # Data layer
│ │ │ │ │ ├── local/ # Room database
│ │ │ │ │ ├── remote/ # Retrofit/Ktor services
│ │ │ │ │ └── repository/ # Repository implementations
│ │ │ │ ├── di/ # Hilt modules
│ │ │ │ ├── domain/ # Business logic
│ │ │ │ │ ├── model/ # Domain models
│ │ │ │ │ ├── repository/ # Repository interfaces
│ │ │ │ │ └── usecase/ # Use cases
│ │ │ │ ├── ui/ # Presentation layer
│ │ │ │ │ ├── feature/ # Feature screens
│ │ │ │ │ │ ├── FeatureScreen.kt # Compose UI
│ │ │ │ │ │ └── FeatureViewModel.kt
│ │ │ │ │ ├── components/ # Reusable Compose components
│ │ │ │ │ └── theme/ # Material theme
│ │ │ │ └── App.kt # Application class
│ │ │ ├── res/
│ │ │ └── AndroidManifest.xml
│ │ ├── test/ # Unit tests
│ │ └── androidTest/ # Instrumentation tests
│ └── build.gradle.kts
├── build.gradle.kts # Project-level build file
├── gradle.properties
├── settings.gradle.kts
└── CLAUDE.md
```
---
## Gradle Configuration (Kotlin DSL)
### App-level build.gradle.kts
```kotlin
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.example.app"
compileSdk = 34
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
}
dependencies {
// Compose BOM
val composeBom = platform("androidx.compose:compose-bom:2024.01.00")
implementation(composeBom)
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Hilt
implementation("com.google.dagger:hilt-android:2.50")
ksp("com.google.dagger:hilt-compiler:2.50")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.9")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("app.cash.turbine:turbine:1.0.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
```
---
## Kotlin Coroutines & Flow
### ViewModel with StateFlow
```kotlin
@HiltViewModel
class UserViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
private val userId: String = checkNotNull(savedStateHandle["userId"])
init {
loadUser()
}
fun loadUser() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
getUserUseCase(userId)
.catch { e ->
_uiState.update {
it.copy(isLoading = false, error = e.message)
}
}
.collect { user ->
_uiState.update {
it.copy(isLoading = false, user = user, error = null)
}
}
}
}
fun clearError() {
_uiState.update { it.copy(error = null) }
}
}
data class UserUiState(
val user: User? = null,
val isLoading: Boolean = false,
val error: String? = null
)
```
### Repository with Flow
```kotlin
interface UserRepository {
fun getUser(userId: String): Flow<User>
fun observeUsers(): Flow<List<User>>
suspend fun saveUser(user: User)
}
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
private val dao: UserDao,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserRepository {
override fun getUser(userId: String): Flow<User> = flow {
// Emit cached data first
dao.getUserById(userId)?.let { emit(it) }
// Fetch from network and update cache
val remoteUser = api.getUser(userId)
dao.insert(remoteUser)
emit(remoteUser)
}.flowOn(dispatcher)
override fun observeUsers(): Flow<List<User>> =
dao.observeAllUsers().flowOn(dispatcher)
override suspend fun saveUser(user: User) = withContext(dispatcher) {
api.saveUser(user)
dao.insert(user)
}
}
```
---
## Jetpack Compose
### Screen with ViewModel
```kotlin
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserScreenContent(
uiState = uiState,
onRefresh = viewModel::loadUser,
onErrorDismiss = viewModel::clearError,
onNavigateBack = onNavigateBack
)
}
@Composable
private fun UserScreenContent(
uiState: UserUiState,
onRefresh: () -> Unit,
onErrorDismiss: () -> Unit,
onNavigateBack: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("User Profile") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
}
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
when {
uiState.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
uiState.user != null -> {
UserContent(user = uiState.user)
}
}
uiState.error?.let { error ->
Snackbar(
modifier = Modifier.align(Alignment.BottomCenter),
action = {
TextButton(onClick = onErrorDismiss) {
Text("Dismiss")
}
}
) {
Text(error)
}
}
}
}
}
```
---
## Sealed Classes for State
### Result Wrapper
```kotlin
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing>
}
fun <T> Result<T>.getOrNull(): T? = (this as? Result.Success)?.data
inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) {
is Result.Success -> Result.Success(transform(data))
is Result.Error -> this
is Result.Loading -> this
}
```
---
## Testing with MockK & Turbine
### ViewModel Tests
```kotlin
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val getUserUseCase: GetUserUseCase = mockk()
private val savedStateHandle = SavedStateHandle(mapOf("userId" to "123"))
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
viewModel = UserViewModel(getUserUseCase, savedStateHandle)
}
@Test
fun `loadUser success updates state with user`() = runTest {
val user = User("123", "John Doe", "john@example.com")
coEvery { getUserUseCase("123") } returns flowOf(user)
viewModel.uiState.test {
val initial = awaitItem()
assertFalse(initial.isLoading)
viewModel.loadUser()
val loading = awaitItem()
assertTrue(loading.isLoading)
val success = awaitItem()
assertFalse(success.isLoading)
assertEquals(user, success.user)
}
}
}
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
```
---
## GitHub Actions
```yaml
name: Android Kotlin CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
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: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Run Detekt
run: ./gradlew detekt
- name: Run Ktlint
run: ./gradlew ktlintCheck
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest
- name: Build Debug APK
run: ./gradlew assembleDebug
```
---
## Lint Configuration
### detekt.yml
```yaml
build:
maxIssues: 0
complexity:
LongMethod:
threshold: 20
LongParameterList:
functionThreshold: 4
TooManyFunctions:
thresholdInFiles: 10
style:
MaxLineLength:
maxLineLength: 120
WildcardImport:
active: true
coroutines:
GlobalCoroutineUsage:
active: true
```
---
## Kotlin Anti-Patterns
- ❌ **Blocking coroutines on Main** - Never use `runBlocking` on main thread
- ❌ **GlobalScope usage** - Use structured concurrency with viewModelScope/lifecycleScope
- ❌ **Collecting flows in init** - Use `repeatOnLifecycle` or `collectAsStateWithLifecycle`
- ❌ **Mutable state exposure** - Expose `StateFlow` not `MutableStateFlow`
- ❌ **Not handling exceptions in flows** - Always use `catch` operator
- ❌ **Lateinit for nullable** - Use `lazy` or nullable with `?`
- ❌ **Hardcoded dispatchers** - Inject dispatchers for testability
- ❌ **Not using sealed classes** - Prefer sealed for finite state sets
- ❌ **Side effects in Composables** - Use `LaunchedEffect`/`SideEffect`
- ❌ **Unstable Compose parameters** - Use stable/immutable types or `@Stable`Related Skills
More skills in Mobile App Development
Android Design Guidelines
Material Design 3 and Android platform guidelines. Use when building Android apps with Jetpack Compose or XML layouts, implementing Material You, navigation, or accessibility. Triggers on tasks involving Android UI, Compose components, dynamic color, or Material Design compliance.
Android Java Skill
Android Java development with MVVM, ViewBinding, and Espresso testing
Appium Skill
Generates production-grade Appium mobile automation scripts for Android and iOS in Java, Python, or JavaScript. Supports real device and emulator testing locally and on TestMu AI cloud with 100+ real devices. Use when the user asks to automate mobile apps, test on Android/iOS, write Appium tests, or mentions "Appium", "mobile testing", "real device", "app automation". Triggers on: "Appium", "mobile test", "Android test", "iOS test", "real device", "app automation", "UiAutomator", "XCUITest driver", "TestMu", "LambdaTest".
Apple Appstore Reviewer
Serves as a reviewer of the codebase with instructions on looking for Apple App Store optimizations or rejection reasons.
App Rejection Recovery
When the user's app or update was rejected by Apple App Review or Google Play Review and they need to diagnose why, fix it, and resubmit fast. Use when the user mentions "app rejected", "App Review rejection", "guideline violation", "Apple rejected my app", "Google Play rejected", "Play policy violation", "Resolution Center", "metadata rejection", "binary rejection", "guideline 2.1", "guideline 4.3", "guideline 5.1.1", "Sign in with Apple required", "Apple ID rejection", "Play Store suspension", "appeal", "I need to respond to App Review", or "expedited review". For pre-submission listing health, see aso-audit. For metadata-only fixes, see metadata-optimization.
App Store Preflight Skills
Scan an iOS/macOS Xcode project for common App Store rejection patterns before submission. Use when preparing an app for App Store review, after receiving a rejection from Apple, or when auditing metadata, subscriptions, privacy manifests, entitlements, or design compliance. Integrates with the asc CLI for metadata inspection.
Explore Other Categories
Skills from other categories with shared topics
Aeo Optimization
AI Engine Optimization - semantic triples, page templates, content clusters for AI citations
Agent Teams
Claude Code Agent Teams - default team-based development with strict TDD pipeline enforcement
Agentic Development
Build AI agents with Pydantic AI (Python) and Claude SDK (Node.js)