Skillquality 0.45

kotlin-patterns

27 Android skills for AI agents (Claude Code, Codex, Cursor). Fixes Supabase auth, Hilt errors, design inconsistency, kapt→ksp, missing UiState states. Reduced my token bills 5×. FitGenZ AI shipped in 18 days.

Price
free
Protocol
skill
Verified
no

What it does

Kotlin Patterns for Android

12 rules for idiomatic, production-safe Kotlin on Android.

Rule 1: Coroutine scope — always use structured concurrency

// ✅ viewModelScope — auto-cancelled when ViewModel is cleared
class MyViewModel : ViewModel() {
    fun load() {
        viewModelScope.launch {
            val result = fetchData()   // suspend, cancellable
        }
    }
}

// ✅ lifecycleScope — tied to Activity/Fragment lifecycle
class MyActivity : ComponentActivity() {
    override fun onStart() {
        super.onStart()
        lifecycleScope.launch {
            viewModel.events.collect { handleEvent(it) }
        }
    }
}

// ❌ GlobalScope — not structured, leaks, not cancellable
GlobalScope.launch { fetchData() }

// ❌ CoroutineScope(Dispatchers.IO) without proper lifecycle binding
val scope = CoroutineScope(Dispatchers.IO)
scope.launch { fetchData() }  // never cancelled

Rule 2: Dispatcher discipline — always switch off Main

// ✅ IO-bound work: switch to IO dispatcher
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
    api.getUser(id)
}

// ✅ CPU-intensive work: use Default dispatcher
suspend fun processLargeList(items: List<Item>): List<Result> = withContext(Dispatchers.Default) {
    items.map { processItem(it) }
}

// ✅ Inject dispatcher for testability
class UserRepository @Inject constructor(
    private val api: UserApi,
    @IoDispatcher private val dispatcher: CoroutineDispatcher
) {
    suspend fun getUser(id: String): User = withContext(dispatcher) {
        api.getUser(id)
    }
}

// ❌ Network call on Main thread — crashes with NetworkOnMainThreadException
suspend fun fetchUser(): User = api.getUser()  // on Main, wrong

Rule 3: StateFlow — expose, never expose MutableStateFlow

// ✅ Mutable private, immutable public
private val _uiState = MutableStateFlow(HomeUiState.Loading)
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

// ✅ Update state correctly
_uiState.value = HomeUiState.Success(items)          // from coroutine on Main
_uiState.update { current -> current.copy(isLoading = false) }  // thread-safe update

// ❌ Exposing MutableStateFlow — external code can change state
val uiState = MutableStateFlow(HomeUiState.Loading)  // anyone can set this

Rule 4: stateIn — convert cold Flow to StateFlow

// ✅ stateIn with correct parameters
val items: StateFlow<List<Item>> = repository.getItemsFlow()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),  // 5s timeout before cancelling
        initialValue = emptyList()
    )

// ✅ WhileSubscribed(5000) — keeps upstream alive 5s after last subscriber
// This handles configuration changes without re-fetching

Rule 5: runCatching — safe error handling without try/catch

// ✅ runCatching wraps any exception in Result<T>
suspend fun getItems(): Result<List<Item>> = runCatching {
    api.getItems().map { it.toDomain() }
}

// ✅ Chain Result transformations
suspend fun getActiveItems(): Result<List<Item>> =
    getItems()
        .map { items -> items.filter { it.isActive } }
        .onFailure { error -> logger.e(error, "Failed to get items") }

// ✅ In ViewModel
viewModelScope.launch {
    getItems()
        .onSuccess { items -> _uiState.value = HomeUiState.Success(items) }
        .onFailure { error -> _uiState.value = HomeUiState.Error(error.message ?: "Error") }
}

// ❌ Try/catch scattered through ViewModel — inconsistent, hard to compose
try {
    val items = api.getItems()
    _uiState.value = HomeUiState.Success(items)
} catch (e: Exception) {
    _uiState.value = HomeUiState.Error(e.message ?: "Error")
}

Rule 6: Sealed interfaces — prefer over sealed classes for state/events

// ✅ sealed interface — no constructor, lighter, more composable
sealed interface LoginResult {
    data object Success : LoginResult
    data class Error(val code: Int, val message: String) : LoginResult
    data object NetworkError : LoginResult
}

// ✅ sealed interface allows a class to implement multiple sealed hierarchies
class AuthError : LoginResult.Error(401, "Unauthorized"), ProfileResult.Unauthorized

// ❌ sealed class — requires a superclass constructor
sealed class LoginResult {
    object Success : LoginResult()
    data class Error(val message: String) : LoginResult()
}

Rule 7: Scope functions — use the right one

// ✅ let — transform nullable value, chain operations
val length = name?.let { it.trim().length } ?: 0

// ✅ apply — configure an object, return the object
val intent = Intent(context, MainActivity::class.java).apply {
    putExtra("id", itemId)
    flags = Intent.FLAG_ACTIVITY_NEW_TASK
}

// ✅ also — side effects, return the same object
val items = repository.getItems().also { logger.d("Loaded ${it.size} items") }

// ✅ run — execute block, return result (combine let + with)
val message = user.run {
    if (isPremium) "Welcome, Premium $name!" else "Welcome, $name!"
}

// ✅ with — call multiple methods on an object, return result
val summary = with(order) {
    "Order #$id: $itemCount items, total: $$total"
}

// ❌ Nested let/run — unreadable, use named functions instead
val result = a?.let { b?.let { c?.run { ... } } }  // pyramid of doom

Rule 8: No !! operator in production code

// ✅ Safe alternatives to !!
val name = user?.name ?: "Anonymous"          // Elvis operator
val id = savedStateHandle.get<String>("id") ?: return  // early return
val file = getFile() ?: throw IllegalStateException("File required")  // explicit throw

// ✅ requireNotNull with message
val config = requireNotNull(buildConfig) { "BuildConfig must be initialized before use" }

// ❌ !! crashes with NullPointerException — never use in production
val name = user!!.name   // NPE if user is null
val id = args!!.getString("id")  // NPE in production

Rule 9: flatMapLatest — cancel previous on new emission

// ✅ flatMapLatest cancels in-flight request when new search query arrives
val searchResults: StateFlow<List<Item>> = searchQuery
    .debounce(300L)
    .flatMapLatest { query ->
        if (query.isBlank()) flowOf(emptyList())
        else repository.search(query)
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

// ❌ flatMapMerge — all emissions run concurrently, results arrive out of order
val searchResults = searchQuery.flatMapMerge { repository.search(it) }

Rule 10: Data classes — correct usage

// ✅ data class for value objects with meaningful equality
data class Item(
    val id: String,
    val title: String,
    val description: String,
    val createdAt: Instant
)

// ✅ copy() for immutable updates
val updated = item.copy(title = "New Title", description = "Updated")

// ✅ data object for singletons in sealed hierarchies
sealed interface AuthState {
    data object Unauthenticated : AuthState
    data object Loading : AuthState
    data class Authenticated(val user: User) : AuthState
}

// ❌ Regular class for domain models — loses equals/hashCode/copy
class Item(val id: String, val title: String)  // no equals, no copy

// ❌ Mutable data class — breaks StateFlow comparison, causes missed updates
data class UiState(var isLoading: Boolean = false)  // var in data class

Rule 11: Lazy initialization

// ✅ by lazy — compute once, reuse, thread-safe by default
val regex: Regex by lazy { Regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") }

// ✅ lateinit var — for dependency injection and test setup
@Inject lateinit var repository: ItemRepository

// ✅ lateinit with isInitialized check when needed
if (::repository.isInitialized) repository.close()

// ❌ lazy without thread-safety consideration in concurrent contexts
val cache by lazy(LazyThreadSafetyMode.NONE) { mutableMapOf<String, Item>() }
// LazyThreadSafetyMode.NONE is only safe if accessed from single thread

Rule 12: Extension functions — don't abuse them

// ✅ Extension for adding behavior to existing types
fun String.isValidEmail(): Boolean =
    android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()

fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, duration).show()
}

fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
    var lastEmission = 0L
    collect { value ->
        val now = System.currentTimeMillis()
        if (now - lastEmission >= windowDuration) {
            lastEmission = now
            emit(value)
        }
    }
}

// ❌ Extension that should be a UseCase — logic belongs in domain
fun List<Item>.filterAndSort(): List<Item> {
    // business logic in extension = untestable, not reusable across modules
    return filter { it.isActive }.sortedBy { it.title }
}

Common Mistakes Quick Reference

❌ Wrong✅ Right
GlobalScope.launchviewModelScope.launch
user!!.nameuser?.name ?: "default"
collectAsState()collectAsStateWithLifecycle()
MutableStateFlow exposed_private.asStateFlow()
Try/catch in ViewModelrunCatching in Repository
var in data classval — immutable
flatMapMerge for searchflatMapLatest
Computation on MainwithContext(Dispatchers.IO)

Capabilities

skillsource-piyushverma0skill-kotlin-patternstopic-agent-skillstopic-ai-agenttopic-androidtopic-antigravitytopic-claude-codetopic-codextopic-cursortopic-gemini-clitopic-hilttopic-jetpack-composetopic-kotlintopic-material3

Install

Quality

0.45/ 1.00

deterministic score 0.45 from registry signals: · indexed on github topic:agent-skills · 8 github stars · SKILL.md body (9,644 chars)

Provenance

Indexed fromgithub
Enriched2026-05-18 19:09:10Z · deterministic:skill-github:v1 · v1
First seen2026-05-18
Last seen2026-05-18

Agent access