Performance
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.
npx -y skills add piyushverma0/android-agent-skills --skill performanceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 14 stars14 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Android app performance for AI agents. Use this skill whenever optimizing app performance, reducing recomposition, Compose stability, @Stable, @Immutable, skippable composables, baseline profiles, R8 optimization, lazy loading, image memory, Coil caching, RecyclerView vs LazyList performance, memory leaks, LeakCanary, ANR prevention, background work, Trace API, systrace, profiling, overdraw, rendering performance, startup time optimization, or any Android performance concern. Apply whenever a screen feels slow or laggy.
SKILL.md
5.9 KB, as published. Nobody here has run it
Android Performance
Rule 1: Compose stability — stop unnecessary recomposition
// ✅ @Stable for classes Compose can't infer stability
@Stable
class ItemState(
val id: String,
val title: String,
var isExpanded: Boolean = false // mutable but tracked
)
// ✅ @Immutable for guaranteed-immutable data (faster than @Stable)
@Immutable
data class Item(
val id: String,
val title: String,
val tags: List<String> // List is not stable by default — @Immutable fixes it
)
// ✅ Use ImmutableList from kotlinx-collections-immutable for stable lists
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
@Composable
fun ItemList(items: ImmutableList<Item>) { // ImmutableList is stable, List is not
LazyColumn {
items(items, key = { it.id }) { item ->
ItemCard(item = item) // won't recompose unless item changes
}
}
}
// ❌ List<Item> parameter causes ItemList to recompose on every parent recomposition
@Composable
fun ItemList(items: List<Item>) { ... }
Rule 2: derivedStateOf — compute only when dependencies change
// ✅ derivedStateOf — prevents recomposition when result doesn't change
@Composable
fun ItemList(items: List<Item>) {
val listState = rememberLazyListState()
// Only recomputes when scroll position crosses threshold
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 3 }
}
// Only recomputes when filter changes, not on every recomposition
val activeItems by remember(items) {
derivedStateOf { items.filter { it.isActive } }
}
Box {
LazyColumn(state = listState) {
items(activeItems, key = { it.id }) { ItemCard(it) }
}
AnimatedVisibility(visible = showScrollToTop, modifier = Modifier.align(Alignment.BottomEnd)) {
ScrollToTopButton(onClick = { /* scroll to top */ })
}
}
}
Rule 3: Baseline Profiles — fast startup
// ✅ Generate with Macrobenchmark — significant startup improvement
// benchmark/src/androidTest/java/BaselineProfileGenerator.kt
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect(packageName = "com.company.app") {
pressHome()
startActivityAndWait()
// Navigate through main flows
device.findObject(By.text("Home")).click()
device.waitForIdle()
}
}
}
// app/build.gradle.kts
dependencies {
implementation(libs.profileinstaller)
}
Rule 4: R8 — enable shrinking in release
// ✅ build.gradle.kts — release config
buildTypes {
release {
isMinifyEnabled = true // R8 removes unused code
isShrinkResources = true // removes unused resources
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
# proguard-rules.pro — keep what R8 would remove incorrectly
-keep class com.company.app.data.remote.dto.** { *; } # keep serialization DTOs
-keepclassmembers class * { @com.google.gson.annotations.SerializedName <fields>; }
# Kotlin
-keep class kotlin.** { *; }
-keepclassmembers class **$WhenMappings { *; }
Rule 5: Image loading performance
// ✅ Coil — proper sizing and caching
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.size(Size.ORIGINAL) // or explicit size: .size(400, 300)
.crossfade(true)
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
)
// ✅ Preload images before they're needed
val imageLoader = LocalContext.current.imageLoader
LaunchedEffect(items) {
items.take(3).forEach { item ->
imageLoader.enqueue(
ImageRequest.Builder(context).data(item.imageUrl).build()
)
}
}
Rule 6: Main thread protection
// ✅ Trace API — mark expensive operations for profiling
suspend fun processData(input: List<Item>): List<ProcessedItem> = withContext(Dispatchers.Default) {
trace("processData") { // visible in Android Studio profiler
input.map { processItem(it) }
}
}
// ✅ Never block main thread — even for "fast" operations
// ❌ This blocks main for DB read:
val item = runBlocking { itemDao.getById(id) }
// ✅ Suspend from coroutine:
val item = withContext(Dispatchers.IO) { itemDao.getById(id) }
Common Mistakes
❌ Passing List<T> to Composable — use ImmutableList<T> or wrap in @Immutable class
❌ Computing in composition — move to remember {} or derivedStateOf {}
❌ isMinifyEnabled = false in release — leaves dead code in APK
❌ Loading full-resolution images — always size images to display size
❌ Running DB queries on Main thread — always withContext(Dispatchers.IO)
❌ No baseline profile — first launch is 40-60% slower without it