Android Kotlin

by alinaqi72a456e6ccf0No license707 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 weeks ago

Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing

Instructions onlySoftware Development
AI-generated overview

Reference guidance for Android Kotlin development with Coroutines, Jetpack Compose, Hilt, Room and MockK testing.

What it does
Provides an Android Kotlin project layout plus code patterns for Gradle Kotlin DSL configuration, Coroutines and Flow, Jetpack Compose screens, sealed-class state, and MockK/Turbine tests. It also includes a GitHub Actions CI example, detekt lint configuration, and a list of Kotlin anti-patterns. It is instructions only and produces no files or scripts.
When to use it
Use when writing or reviewing Android Kotlin source files, such as .kt, .kts, build.gradle.kts, or files under an android directory. It suits work involving Compose UI, Hilt dependency injection, Room, or coroutine-based state and testing.
Requirements
No scripts or bundled assets; it is a Markdown instruction document. Applying its examples assumes an Android project with Gradle, JDK 17, the Android SDK, and the listed libraries (Compose, Hilt, Room, Coroutines, MockK, Turbine).

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
@HiltViewModelclass 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
@Composablefun UserScreen(    viewModel: UserViewModel = hiltViewModel(),    onNavigateBack: () -> Unit) {    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    UserScreenContent(        uiState = uiState,        onRefresh = viewModel::loadUser,        onErrorDismiss = viewModel::clearError,        onNavigateBack = onNavigateBack    )}
@Composableprivate 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", "[email protected]")        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

Source and attribution

Source:alinaqi/maggyinskills/android-kotlinat commit72a456e

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

Android Kotlin Agent Skill | SourceWeft