Claude Agent Skill · by Alinaqi

Android Kotlin

Install Android Kotlin skill for Claude Code from alinaqi/claude-bootstrap.

Install
Terminal · npx
$npx skills add https://github.com/alinaqi/claude-bootstrap --skill android-kotlin
Works with Paperclip

How Android Kotlin fits into a Paperclip company.

Android Kotlin drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md457 lines
Expand
---name: android-kotlindescription: Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testingwhen-to-use: When working on Android Kotlin source filesuser-invocable: falsepaths: ["**/*.kt", "**/*.kts", "android/**", "**/build.gradle.kts"]effort: medium--- # 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```kotlinplugins {    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```kotlininterface 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```kotlinsealed 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 ```yamlname: 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```yamlbuild:  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`