Skill

Write idiomatic Kotlin + Compose UI code with efficiency patterns

Idiomatic-efficiency guidelines for Kotlin and Compose across collections, null safety, functions, classes, and coroutines.

Works with kotlincompose

Maintainer of this project? Claim this page to edit the listing.


91
Spark score
out of 100
Updated 26 days ago
Version 13.6.1

Add to Favorites

Why it matters

Generate clean, idiomatic Kotlin code with Jetpack Compose UI that follows modern Android development best practices, leveraging language features and framework conventions to produce maintainable, efficient applications.

Outcomes

What it gets done

01

Write type-safe Kotlin code using null safety, data classes, and extension functions

02

Build declarative Compose UI with state management and recomposition optimization

03

Apply idiomatic patterns like sealed classes, coroutines, and flow for async operations

04

Structure code following Kotlin conventions for readability and maintainability

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/ag-kotlin | bash

Overview

Kotlin + Compose: Idiomatic Efficiency Reference

Pairs verbose or non-idiomatic Kotlin and Compose patterns against their preferred equivalents across collections, null safety, functions, classes, coroutines, and Compose UI, plus a table of smaller Kotlin-specific anti-patterns. Use it when writing, reviewing, or refactoring Kotlin or Compose code toward idiomatic style. It covers language-level patterns only, not architectural decisions - apply judgment against readability.

What it does

Gives idiomatic-efficiency guidelines for Kotlin and Jetpack Compose across seven areas: collections and data transformation, null safety, functions and lambdas, classes and objects, coroutines and Flow, Compose UI, and Kotlin-specific anti-patterns. Each area pairs a verbose or non-idiomatic pattern directly against the preferred Kotlin equivalent.

When to use - and when NOT to

Use this skill when a task matches language-specific super-code guidelines for Kotlin - reviewing, writing, or refactoring Kotlin or Compose code toward idiomatic style. These are language-specific guidelines only and do not cover overall architectural decisions, and over-compression can reduce readability, so judgment should still be applied rather than mechanically collapsing every pattern regardless of context.

Inputs and outputs

Collections favor scope functions and standard-library transforms over imperative loops: a manual filter-and-uppercase loop becomes a filter chained into a map, a manual grouping loop becomes groupBy, and a manual running-total loop becomes sumOf, alongside preferring associate, associateBy, partition, flatMap, and zip over hand-rolled equivalents:

val result = items.filter { it.isActive }.map { it.name.uppercase() }
val map = items.groupBy { it.category }
val total = orders.sumOf { it.amount }

Null safety favors the Elvis operator over an explicit null-check-then-assign, a safe call chained into let over a double null check, and an explicit requireNotNull contract or an early return over an unguarded double-bang assertion - the reasoning throughout is to push the non-null guarantee to the boundary where a value first enters a function, rather than scattering defensive checks and force-unwraps deep inside the logic where a missed one silently crashes at runtime. Functions favor a single-expression function body over an unnecessary block-and-return, dropping an unused lambda parameter that adds noise without adding meaning, and using apply to configure a builder object across several statements instead of repeating the receiver name on every line. Scope functions generally, meaning let, run, apply, also, and with, are preferred over repeated receiver references, but capped deliberately at no more than two levels of nesting, since stacking scope functions past that point trades a small amount of verbosity for a real readability cost that outweighs the savings.

Integrations

Classes and objects favor a data class over a hand-written mutable class for immutable data, since a data class gives free equality, hashing, and copy semantics that a hand-rolled mutable class doesn't; a private top-level constant over a companion object that exists only to hold one value, since the companion object adds an extra allocation and lookup indirection for no real benefit when nothing else lives in it; and putting display logic directly on an enum's own constructor rather than a separate when statement that has to be kept in sync by hand in two different places whenever a new variant is added. Sealed classes or interfaces are preferred over a plain enum whenever the variants actually need to carry different data alongside them, since an enum's constructor is shared identically across every variant. Coroutines and Flow favor a plain suspend function call over an unnecessary async/await pair when the result is consumed immediately afterward, since the async wrapper adds concurrency overhead that buys nothing if nothing else runs in parallel with it; consumeEach or collect over a manual receive loop that has to hand-manage its own termination condition; and an atomic update block for StateFlow mutation over manually reassigning the backing value property in many separate call sites, which is both more verbose and more prone to a lost update under concurrent access. Coroutines should never be launched from a constructor or an init block, since that ties the coroutine's lifecycle to object construction rather than to an explicit, cancellable scope, and GlobalScope should never be used for the same underlying reason: it has no lifecycle to cancel against. Compose UI favors skipping remember for cheap derived state and reserving it specifically for computation that's genuinely expensive or that allocates a new object, since remember itself has a real cost that isn't free; passing only the specific field a composable actually needs rather than an entire state object, which improves both parameter stability and how narrowly a recomposition can be scoped; remembering a click-handler lambda rather than letting a fresh lambda instance be created on every single recomposition; and flattening a Column or Row that's nested purely to group children with no layout purpose of its own. Compose also calls for LazyColumn or LazyRow specifically for lists of unknown or large size, and warns against ever nesting a LazyColumn inside a Column with unbounded height, since that defeats the whole point of lazy layout. A dedicated anti-pattern table covers a further set of smaller, more mechanical Kotlin-specific issues: comparing a boolean value to true explicitly instead of just using the boolean directly, a double-bang assertion used right after a null check instead of the Elvis operator, calling toMutableList on an empty list literal instead of the direct mutableListOf constructor, a single-branch when-with-else used where a plain if/else reads more clearly, a redundant toString call on a value that's already a string, an explicitly written return type of Unit that the compiler already infers on its own, a manual object-expression implementation of a functional interface like Runnable instead of the shorter SAM-conversion form, the JvmStatic annotation applied in pure Kotlin code where it serves no purpose outside genuine Java interop, and wrapping every function body in its own logging try/catch instead of handling errors once at a shared boundary.

Who it's for

Kotlin and Jetpack Compose developers who want a fast before-and-after reference for idiomatic style across collections, null handling, functions, classes, coroutines, and Compose UI, rather than re-deriving each convention from scratch during review.

Source README

Kotlin + Compose: Idiomatic Efficiency Reference

When to Use

  • Use this skill when the task matches this description: Language-specific super-code guidelines for kotlin.

Table of Contents

  1. Collections & Data Transformation
  2. Null Safety
  3. Functions & Lambdas
  4. Classes & Objects
  5. Coroutines & Flow
  6. Compose UI
  7. Anti-patterns specific to Kotlin

1. Collections & Data Transformation {#collections}

Prefer scope functions and stdlib transforms over imperative loops.

// ❌ Verbose
val result = mutableListOf<String>()
for (item in items) {
    if (item.isActive) {
        result.add(item.name.uppercase())
    }
}

// ✅ Idiomatic
val result = items.filter { it.isActive }.map { it.name.uppercase() }
// ❌ Manual grouping
val map = mutableMapOf<String, MutableList<Item>>()
for (item in items) {
    map.getOrPut(item.category) { mutableListOf() }.add(item)
}

// ✅
val map = items.groupBy { it.category }
// ❌ Manual fold
var total = 0
for (order in orders) total += order.amount

// ✅
val total = orders.sumOf { it.amount }

Use associate, associateBy, partition, flatMap, zip instead of manual equivalents.


2. Null Safety {#null-safety}

// ❌ Unnecessary null check when Elvis suffices
val name: String
if (user?.name != null) {
    name = user.name
} else {
    name = "Guest"
}

// ✅
val name = user?.name ?: "Guest"
// ❌ Double null check
if (response != null && response.body != null) {
    process(response.body!!)
}

// ✅
response?.body?.let { process(it) }
// ❌ !! without guard
val value = nullable!!.doSomething()

// ✅ Make the non-null contract explicit at the boundary
val value = requireNotNull(nullable) { "nullable must be set before calling X" }.doSomething()
// Or return early:
val n = nullable ?: return

3. Functions & Lambdas {#functions}

// ❌ Single-expression function with unnecessary block body
fun double(x: Int): Int {
    return x * 2
}

// ✅
fun double(x: Int) = x * 2
// ❌ Lambda capturing unused parameter
items.forEach { item -> doSomething() }

// ✅
items.forEach { doSomething() }
// ❌ Redundant with/apply nesting
val builder = Builder()
builder.setName("x")
builder.setAge(1)
val result = builder.build()

// ✅
val result = Builder().apply {
    setName("x")
    setAge(1)
}.build()

Prefer let, run, apply, also, with over repeated receiver references - but don't nest more than 2 levels deep.


4. Classes & Objects {#classes}

// ❌ Mutable class for immutable data
class Point {
    var x: Int = 0
    var y: Int = 0
}

// ✅
data class Point(val x: Int, val y: Int)
// ❌ Companion object just to hold a constant
class Foo {
    companion object {
        val TAG = "Foo"
    }
}

// ✅ — top-level if only used in this file
private const val TAG = "Foo"
class Foo
// ❌ Enum with when that has to be updated in two places
enum class Status { ACTIVE, INACTIVE }
fun label(s: Status) = when(s) { Status.ACTIVE -> "Active"; Status.INACTIVE -> "Inactive" }

// ✅ — put display logic on the enum itself
enum class Status(val label: String) { ACTIVE("Active"), INACTIVE("Inactive") }

Sealed classes/interfaces over enum when variants carry different data.


5. Coroutines & Flow {#coroutines}

// ❌ Unnecessary async/await pair when result is used immediately
val result = async { fetchData() }.await()

// ✅
val result = fetchData() // just suspend fun, no async needed
// ❌ Collecting in a loop
while (true) {
    val value = channel.receive()
    process(value)
}

// ✅
channel.consumeEach { process(it) }
// or for Flow:
flow.collect { process(it) }
// ❌ StateFlow + manual emit boilerplate
private val _state = MutableStateFlow(initial)
val state: StateFlow<State> = _state
// ... in many places: _state.value = newValue

// ✅ — use update{} for atomic mutation
_state.update { it.copy(field = newValue) }

Don't launch coroutines in constructors or init blocks. Don't use GlobalScope.


6. Compose UI {#compose}

// ❌ Unnecessary remember for derived state that's cheap to compute
val displayName = remember { user.firstName + " " + user.lastName }

// ✅ — only remember if computation is expensive or involves object creation
val displayName = "${user.firstName} ${user.lastName}"
// ❌ Passing entire state object when composable only needs one field
@Composable
fun UserBadge(user: User) { Text(user.name) }

// ✅ — pass only what's needed (stability + minimal recomposition)
@Composable
fun UserBadge(name: String) { Text(name) }
// ❌ Inline click handler lambda (creates new instance each recomposition)
Button(onClick = { viewModel.onSave() }) { ... }

// ✅
val onSave = remember { { viewModel.onSave() } }
Button(onClick = onSave) { ... }
// Or pass it down as a parameter already
// ❌ Nested Column/Row just to group children
Column {
    Column {
        Text("a")
        Text("b")
    }
}

// ✅
Column {
    Text("a")
    Text("b")
}

Use LazyColumn/LazyRow for lists of unknown or large size. Never put a LazyColumn inside a Column with unbounded height.


7. Anti-patterns specific to Kotlin {#antipatterns}

Anti-pattern Preferred
if (x == true) if (x)
if (x == null) return else x!! val x = x ?: return
listOf().toMutableList() for a known-size list mutableListOf()
when with a single branch and else if/else
.toString() on a string remove it
Explicit Unit return type on functions omit (inferred)
object : Runnable { override fun run() { ... } } Runnable { ... } (SAM)
@JvmStatic in pure Kotlin code only needed for Java interop
Wrapping every function in try/catch to log handle at the boundary, not inside

Limitations

  • These are language-specific guidelines and do not cover overall architectural decisions.
  • Over-compression might reduce readability; apply judgement.

FAQ

Common questions

Discussion

Questions & comments · 0

Sign In Sign in to leave a comment.