android-kotlin

Modern Android development with Kotlin coroutines, Jetpack Compose, dependency injection, and structured testing. Covers three-layer architecture (data, domain, UI) with Hilt for dependency injection and Room for local persistence Jetpack Compose for declarative UI with StateFlow-based state management and lifecycle-aware collection patterns Coroutines and Flow for asynchronous operations, including repository patterns with network-first and cache-first strategies MockK and Turbine for unit testing ViewModels, repositories, and flows with proper test dispatcher setup Includes Gradle Kotlin DSL configuration, detekt/ktlint linting rules, and GitHub Actions CI pipeline

INSTALLATION
npx skills add https://github.com/alinaqi/claude-bootstrap --skill android-kotlin
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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

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

@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

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

@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

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 &#x26; Turbine

ViewModel Tests

@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

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

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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card