К основному содержанию
скилл CX Codex

android-kotlin — скилл для Claude Code: Android на Kotlin

Подсказывает агенту структуру Android-проекта на Kotlin: Coroutines, Jetpack Compose, Hilt, Room и тесты на MockK. Включается сам при работе с файлами .kt.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add alinaqi/maggy --skill android-kotlin -a claude-code
Открыть на GitHub

Что делает android-kotlin

Скилл подхватывается, когда агент открывает файлы **/*.kt, **/*.kts, android/** или **/build.gradle.kts, — отдельно вызывать его не нужно. Внутри лежат готовые образцы: дерево модулей с разделением на data, domain и ui, конфигурация build.gradle.kts на Kotlin DSL с плагинами Hilt и KSP, ViewModel на StateFlow, репозиторий с Flow и flowOn, экран на Compose с hiltViewModel() и collectAsStateWithLifecycle().

Зависимости в примерах зафиксированы: Compose BOM 2024.01.00, Hilt 2.50, Room 2.6.1, MockK 1.13.9, coroutines 1.7.3, Java 17, compileSdk 34 и minSdk 24. Если ваш проект живёт на других версиях, агент может потянуть за собой эти — это повод сверить diff перед коммитом.

Тестовые примеры опираются на MockK и Turbine, а для инструментальных — на ui-test-junit4. Полезно, когда нужно быстро накидать каркас фичи или привести разросшийся модуль к слоистой структуре.

Какие задачи решает

  • разложить фичу по слоям data, domain и ui
  • написать ViewModel на StateFlow с обработкой ошибок
  • собрать репозиторий с кэшем и сетевым запросом на Flow
  • подключить Hilt и KSP в build.gradle.kts
  • покрыть ViewModel тестами на MockK и Turbine

Примеры запросов агенту

  • добавь экран профиля по структуре из скилла android-kotlin
  • сделай ViewModel с StateFlow и обработкой ошибок, как в android-kotlin
  • настрой Hilt и Room в build.gradle.kts по образцу скилла

Как установить android-kotlin

1Через Skills CLI (Vercel)

npx skills add alinaqi/maggy --skill android-kotlin -a claude-code

2Вручную: положить файлы

~/.claude/skills/android-kotlin/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/android-kotlin/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о android-kotlin

Как установить android-kotlin в Claude?

Выполните npx skills add alinaqi/maggy --skill android-kotlin -a claude-code. Скилл окажется в ~/.claude/skills/android-kotlin/SKILL.md.

Что делает android-kotlin?

Даёт агенту шаблоны Android-проекта на Kotlin: структуру пакетов, Gradle на Kotlin DSL, ViewModel на StateFlow, Compose-экраны, Hilt и тесты на MockK.

Когда скилл включается сам?

При работе с файлами **/*.kt, **/*.kts, android/** и **/build.gradle.kts. Вручную вызывать его не нужно.

Исходный текст

SKILL.md · MIT
---
name: android-kotlin
description: Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing
when-to-use: When working on Android Kotlin source files
user-invocable: false
paths: ["**/*.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
```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", "[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`

© alinaqi · MIT · alinaqi/maggy

Похожие скиллы

Всё по теме «Мобильная разработка» →
android-development dpconde Скилл ведёт агента по официальной архитектуре Android: модули, Jetpack Compose, Hilt, Room. Пригодится, когда проект на Kotlin растёт и пора навести порядок в слоях. CX Codex +1 718установок android-jetpack-compose-cursorrules-prompt-file PatrickJS Правило для Cursor, которое задаёт агенту ориентиры по Android-разработке на Jetpack Compose: архитектура, состояние, тесты. ★ 41 011звёзд репо android-kotlin-development aj-geddes Скилл для ИИ-агента, который ведёт разработку нативного Android-приложения на Kotlin: MVVM, Compose, Retrofit, Room. CX Codex +1 1 500установок kotlin-testing affaan-m Скилл задаёт агенту шаблоны тестирования Kotlin: Kotest, MockK, корутины, property-based тесты и покрытие через Kover. Пригодится, когда пишете тесты в TDD-цикле. CX Codex +1 4 700установок kotlin-specialist https://github.com/Jeffallan Скилл направляет ИИ при разработке на Kotlin: корутины, Flow, KMP, Compose и Ktor. Пригодится для реализации функций с проверками стиля и отмены задач. CX Codex +1 5 200установок kotlin-coroutines-flows affaan-m Скилл подсказывает паттерны Kotlin Coroutines и Flow для Android и KMP. Пригодится при отладке отмены, параллельных задач и тестов. CX Codex +1 4 800установок

Из того же набора32

pwa-development Скилл для сборки Progressive Web App: манифест, service worker, стратегии кэширования и офлайн-режим. Пригодится, когда приложение должно ставиться на телефон и открываться без сети. CX Codex +1 4 100установок code-review Запускает автоматическое ревью кода перед коммитами или деплоем. Сверяет правки с архитектурными решениями и находит скрытые баги. CX Codex +1 1 400установок ui-mobile Задаёт агенту правила мобильного интерфейса: минимальный размер нажатия 44×44, контраст по WCAG 2.1 AA, видимые границы кнопок. CX Codex +1 1 200установок posthog-analytics Скилл для внедрения продуктовой аналитики PostHog: события, идентификация пользователей, фича-флаги и дашборды. Пригодится, когда нужно добавить трекинг в проект. CX Codex +1 809установок flutter Скилл для работы с Flutter и Dart: подсказывает структуру проекта, провайдеры Riverpod, модели Freezed и навигацию go_router. CX Codex +1 736установок android-java Скилл для работы с Android-проектами на Java: подсказывает структуру MVVM, ViewBinding и тесты на Espresso. CX Codex +1 667установок supabase Скилл для работы с Supabase CLI, миграциями PostgreSQL, политиками RLS и Deno Edge Functions. Пригодится при локальной разработке бэкенда. CX Codex +1 645установок code-deduplication Скилл ведёт CODE_INDEX.md с функциями и компонентами проекта, чтобы агент проверял, что уже написано, до создания новой утилиты. CX Codex +1 581установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой