Install
Terminal · npx$
npx skills add https://github.com/alinaqi/claude-bootstrap --skill android-kotlinWorks 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 packSource file
SKILL.md457 linesExpandCollapse
---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`