Most Android developers already use an LLM daily, but the difference between a prompt that saves an hour and one that produces plausible-looking garbage is context. A model that doesn't know your Gradle version, your minSdk, or your DI framework will happily suggest rememberSaveable where you needed a ViewModel, or emit a Room migration that drops a column. The prompts below are the ones that survive contact with a real codebase.
Each one follows the same pattern: constraint first, task second, output format last. That ordering matters — if you tell the model "write a Compose screen" before you tell it "minSdk 24, Material 3, no Accompanist", it defaults to whatever was popular in its training data.
Below are 12 templates with real usage examples. They are not magic — they are context packagers. The value is in what you paste alongside them.
1. The Compose screen scaffold with explicit state contract
Compose prompts fail most often because the model conflates UI state and business logic. Force a separation up front.
Prompt:
You are an Android developer working on a Kotlin app with Jetpack Compose, Material 3, minSdk 24, Kotlin 2.x. Generate a screen composable named
XScreenthat takes a singlestate: XUiStateparameter and anonEvent: (XEvent) -> Unitcallback. Do not put anyremember { mutableStateOf }for business data inside the composable. Provide: the sealedXUiState(Loading, Content, Error), the sealedXEvent, and a stateless@Previewfor each state. UsestringResourcefor every user-facing string.
Usage example:
sealed interface ProfileUiState {
data object Loading : ProfileUiState
data class Content(val name: String, val email: String) : ProfileUiState
data class Error(val message: String) : ProfileUiState
}
Why it works: the explicit "no remember for business data" clause kills the single most common Compose anti-pattern the model reproduces. You pay for it with one extra parameter in every screen — that is the trade-off, and it is worth it.
mutableStateOf inside the composable. If the model ignored the constraint, it means your pasted context did not include the state class — re-send with the sealed interface inline.
2. State hoisting refactor
When you inherit a screen where state is smeared across three composables, this prompt untangles it.
Prompt:
Here is a composable with state scattered across nested children. Refactor it so all state lives at the top-level screen composable and children receive values plus lambdas. Keep the public API of the screen identical. List every state variable you moved and where it now lives. Do not change behavior.
Usage example: feed the model a ProductListScreen where searchQuery lives inside a SearchBar child. Output should move searchQuery to the screen and pass query: String, onQueryChange: (String) -> Unit down. The "list every variable you moved" instruction is what makes review fast — you diff the list against the code instead of reading 200 lines.
3. Coroutine dispatcher and cancellation audit
This is the prompt that catches real bugs. Most LLM-suggested coroutine code is correct syntactically and wrong semantically.
Prompt:
Review the following Kotlin coroutine code for: (1) blocking calls on Dispatchers.Main, (2) missing
withContextaround IO, (3)GlobalScopeusage, (4) swallowedCancellationExceptioninside try/catch, (5)launchwhereasyncis needed for a result. For each finding give the line, the risk, and a corrected snippet. If nothing is wrong, say so explicitly — do not invent issues.
Usage example:
// Problem snippet the model should flag
viewModelScope.launch {
try {
val data = repository.load() // does file IO
_state.value = data
} catch (e: Exception) {
_error.value = e.message
}
}
The model should flag two things: the IO on the default Main dispatcher, and catch (e: Exception) swallowing CancellationException. That second one is the non-obvious one — it means your coroutine keeps running after the ViewModel is cleared.
CancellationException is a subclass of Exception. A blanket catch (e: Exception) catches it silently and breaks structured concurrency. Rethrow it or catch IOException specifically.
4. Room entity, DAO, and migration in one shot
Room prompts collapse when you ask for the entity without the schema context. Always paste the current @Database version and entity list.
Prompt:
Current database: version 7, entities [User, Order, Product]. Add a
tags: List<String>field toProduct. Generate: the updated entity with a@TypeConvertersforList<String>, the migration from 7 to 8 usingALTER TABLE, and the updated@Databaseannotation. Do not usefallbackToDestructiveMigration. Show the converter class separately.
Usage example: the converter is the part people forget:
class StringListConverter {
@TypeConverter fun fromList(value: List<String>): String =
value.joinToString("|")
@TypeConverter fun toList(value: String): List<String> =
if (value.isEmpty()) emptyList() else value.split("|")
}
The `
|delimiter is a trap if your tags can contain|` — which is why the prompt should also ask the model to name the assumption. A good follow-up: "What input would break this converter?"
5. Query optimization against an index plan
Room performance questions are usually answered badly because the model doesn't see your indexes.
Prompt:
Here is my entity with its indexes and three queries. For each query, state whether it can use an index, whether it does a full table scan, and where a
@Transactionor@Relationwould be better. Then propose the minimal index change that helps the slowest query. Paste the query plan reasoning as a table.
Usage example: a query like SELECT * FROM Order WHERE userId = :id ORDER BY createdAt DESC benefits from a composite index (userId, createdAt) — not two separate indexes. The model will usually get this right only if you ask it to reason about ordering, so include that instruction.
6. ViewModel + StateFlow with a proper lifecycle
Prompt:
Generate a ViewModel exposing
StateFlow<UiState>built withstateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), Initial). Load data ininitviaviewModelScope.launch. Provide aretry()function. Explain whyWhileSubscribed(5_000)and notEagerlyin one sentence — I want to confirm the trade-off, not just the code.
That final sentence forces the model to justify the choice. WhileSubscribed(5_000) stops upstream collection 5 seconds after the last subscriber leaves — good for config changes, but if you have expensive upstream work you pay for restarting it.
7. Gradle build file diagnosis
Prompt:
Here is my
app/build.gradle.kts. The build fails with [paste exact error]. Identify the likely cause, list any version conflicts between Kotlin, Compose Compiler, and AGP, and give the corrected block. Do not upgrade anything I did not ask about.
Usage example: the classic failure is a Compose Compiler version mismatch with the Kotlin version. The last sentence prevents the model from rewriting half your file — a real failure mode where you accept a "fix" that silently bumps AGP and breaks CI.
8. Test generation with fakes, not mocks
Prompt:
Write a JUnit test for
UserRepositoryImpl. Use a hand-written fakeUserApi(no Mockito, no MockK). Cover: success path, empty response, and network exception mapped to a domain error. Usekotlinx-coroutines-testwithrunTestandStandardTestDispatcher. Include the fake class in full.
Usage example: the fake forces the model to define the interface, which usually exposes a design smell — if the fake is 80 lines, your interface is too wide. That is a free architectural review you did not ask for.
9. Compose recomposition debugging
Prompt:
This composable recomposes too often. Explain which parameters are unstable, why, and give three fixes ordered by effort. Assume I cannot add
@Stableto third-party classes. Show the fix usingrememberkeys orderivedStateOfwhere appropriate.
Usage example: passing a List<Item> to a LazyColumn item triggers recomposition because List is unstable. Fix: wrap in ImmutableList or use kotlinx.collections.immutable. The model will often suggest @Immutable on the data class first — that works only if all fields are truly immutable, which is why the "cannot add @Stable to third-party classes" constraint matters.
10. Navigation graph generation
Prompt:
Generate a Navigation Compose graph for screens [Home, Detail(id), Settings]. Use type-safe routes with
@Serializabledata classes (Navigation 2.8+). Show theNavHost, the route definitions, and one example of navigating with an argument. No string routes.
Usage example:
@Serializable data class Detail(val id: Long)
composable<Detail> { backStackEntry ->
val route = backStackEntry.toRoute<Detail>()
DetailScreen(route.id)
}
Type-safe navigation removes the class of bugs where a route string typo compiles fine and crashes at runtime.
11. Code review against a checklist
This is the prompt to run before opening a PR. It pairs well with the idea of a second reviewer that never sleeps — see 12 prompts that catch bugs before the merge button lights up for a broader version.
Prompt:
Review this diff as a senior Android reviewer. Check for: main-thread IO, memory leaks in
Contextreferences, missing null handling from platform APIs, hardcoded strings, and@Suppresshiding a real warning. Output a table: line, severity, issue, suggested fix. Do not comment on style.
Usage example: the Context leak check catches the classic object Singleton { lateinit var ctx: Context } pattern that survives many reviews.
12. ADR (architecture decision record) drafting
Prompt:
I am choosing between [option A] and [option B] for [specific decision]. Draft an ADR: context, options, decision, consequences. Two paragraphs max per section. State the trade-off I am paying for the chosen option explicitly.
Usage example: deciding between a single-module and multi-module app. The model will produce the boilerplate; you supply the real constraint (build time, team size). The trade-off sentence is what makes the ADR useful six months later.
What these prompts have in common
Every one of them packs context the model cannot infer: your schema version, your dispatcher choices, your minSdk, your DI setup. The ones that fail — and they will fail sometimes — fail because you skipped context, not because the model is bad.
One honest limitation: an LLM will confidently generate a Room migration that compiles but corrupts data if you did not paste the current schema. Always run migrations against a real database with data before shipping. The prompt is a draft generator, not a verifier.
Start with prompt 3 (coroutine audit) and prompt 11 (PR review) — those two pay back their context cost fastest on any Kotlin codebase, because they catch the bugs that pass CI and crash in production.
If you want a broader set of developer-facing prompt patterns, the From Idiomatic Go to Data Race Hunting covers a similar constraint-first approach in a different ecosystem.