Skip to main content

Cheat Sheets

Quick references for the APIs you use daily. Print-friendly, scannable, no fluff.

Compose state APIs

// Hold state across recompositions
var count by remember { mutableIntStateOf(0) }

// Survive config change (rotation)
var draft by rememberSaveable { mutableStateOf("") }

// Survive process death (ViewModel)
val state by viewModel.state.collectAsStateWithLifecycle()

// Compute from other state; only re-emit when result changes
val showFab by remember { derivedStateOf { listState.firstVisibleItemIndex > 10 } }

// Convert async source to State
val image by produceState<ImageBitmap?>(initialValue = null, key1 = userId) {
value = imageLoader.load(userId)
}

// Bridge Compose state to coroutine world
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.collect { analytics.logScroll(it) }
}

Compose side effects

APIWhen
LaunchedEffect(key)Coroutine tied to composition, restarts on key change
rememberCoroutineScope()Launch from event handlers (click)
DisposableEffect(key)Setup + cleanup pair
SideEffectRun after every successful recomposition
rememberUpdatedState(value)Capture latest value in long-running effects

Coroutines

// Structured concurrency
coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
a.await() + b.await() // both must succeed
}

supervisorScope {
launch { fetchOptionalA() } // failure isolated
launch { fetchOptionalB() }
}

// Dispatchers
withContext(Dispatchers.IO) { File("...").readBytes() }
withContext(Dispatchers.Default) { heavyCompute() }
withContext(Dispatchers.Main) { uiUpdate() }

// Cancellation
viewModelScope.launch {
try {
doWork()
} catch (c: CancellationException) {
throw c // always re-throw
} catch (e: Exception) {
handle(e)
}
}

// Timeout
withTimeout(5.seconds) { api.fetch() }
withTimeoutOrNull(5.seconds) { api.fetch() } // returns null on timeout

Flow operators

flow // builder
.map { transform } // T → R
.filter { predicate } // keep matching
.distinctUntilChanged() // drop consecutive duplicates
.debounce(300) // wait for quiet
.throttleFirst(500) // leading edge throttle
.sample(1000) // fixed-interval emit
.combine(other) { a, b -> a to b } // latest from each
.zip(other) { a, b -> a to b } // pair 1:1
.merge(other) // interleave
.flatMapConcat { f } // serial flatten
.flatMapMerge(concurrency = 4) { f } // parallel
.flatMapLatest { f } // cancel previous (user input)
.onStart { /* setup */ }
.onCompletion { cause -> /* teardown */ }
.catch { e -> emit(default) }
.retry(3) { it is IOException }
.flowOn(Dispatchers.IO) // upstream dispatcher
.stateIn(scope, SharingStarted.WhileSubscribed(5_000), initialValue)

Hilt cheats

@HiltAndroidApp class App : Application()

@AndroidEntryPoint class MainActivity : ComponentActivity()
@AndroidEntryPoint class MyFragment : Fragment()
@AndroidEntryPoint class WorkerService : Service()

@HiltViewModel
class MyViewModel @Inject constructor(
private val repo: Repository
) : ViewModel()

// Modules
@Module @InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()...
}

@Module @InstallIn(SingletonComponent::class)
abstract class RepoModule {
@Binds @Singleton
abstract fun bind(impl: RepositoryImpl): Repository
}

// Qualifiers
@Qualifier annotation class IoDispatcher
@Provides @IoDispatcher fun io() = Dispatchers.IO

// Assisted injection
class MyVm @AssistedInject constructor(
@Assisted val id: String,
private val repo: Repo
) {
@AssistedFactory
interface Factory {
fun create(id: String): MyVm
}
}

// Testing
@HiltAndroidTest
class MyTest {
@get:Rule(order = 0) val hilt = HiltAndroidRule(this)
@get:Rule(order = 1) val compose = createAndroidComposeRule<HiltTestActivity>()

@BindValue @JvmField val repo: Repository = FakeRepo()
}

Hilt scopes

ScopeLifetime
@SingletonApp process
@ActivityRetainedScopedSurvives rotation, not process death
@ViewModelScopedPer ViewModel
@ActivityScopedPer Activity
@FragmentScopedPer Fragment
@ServiceScopedPer Service
@ViewScopedPer View

Modifier order — the rule

Modifier
.size(100.dp) // 1. Size
.clip(RoundedCornerShape(8.dp)) // 2. Clip
.background(Color.Blue) // 3. Background (after clip)
.border(1.dp, Color.Black) // 4. Border
.clickable { } // 5. Click
.padding(16.dp) // 6. Padding (last; affects children only)

Modifier order applies left-to-right. Padding after background adds padding inside; before, around.

Compose performance checklist

// Stable inputs
@Immutable data class UiState(val items: ImmutableList<Item>)

// Stable callbacks
val onClick = remember { { id: String -> vm.click(id) } }

// Stable keys + content type
items(items, key = { it.id }, contentType = { "item" }) { ... }

// Derive expensive computations
val canSubmit by remember { derivedStateOf { email.isValid && pw.isStrong } }

// Defer state reads
Modifier.graphicsLayer { translationX = scrollState.value.toFloat() }

// GPU transforms
Modifier.graphicsLayer { rotationZ = rotation } // not .rotate(rotation)

Room cheats

@Entity(tableName = "users", indices = [Index("email", unique = true)])
data class UserEntity(@PrimaryKey val id: String, val email: String, val name: String)

@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getById(id: String): UserEntity?

@Query("SELECT * FROM users")
fun observeAll(): Flow<List<UserEntity>> // auto-emits on table change

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(user: UserEntity)

@Update suspend fun update(user: UserEntity)
@Delete suspend fun delete(user: UserEntity)

@Query("DELETE FROM users WHERE id = :id")
suspend fun deleteById(id: String)

@Transaction
suspend fun insertWithProfile(user: UserEntity, profile: ProfileEntity) {
upsert(user)
upsertProfile(profile)
}

// Relations
@Transaction
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getWithProfile(id: String): UserWithProfile

// Paging 3
@Query("SELECT * FROM users ORDER BY name")
fun pagingSource(): PagingSource<Int, UserEntity>
}

@Database(entities = [UserEntity::class], version = 2, exportSchema = true)
@TypeConverters(MyConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE users ADD COLUMN avatar TEXT")
}
}
}
}

OkHttp / Retrofit cheats

val client = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(HttpLoggingInterceptor().setLevel(BODY))
.cache(Cache(File(cacheDir, "http"), 50L * 1024 * 1024))
.connectTimeout(10.seconds.toJavaDuration())
.readTimeout(30.seconds.toJavaDuration())
.certificatePinner(pinner)
.build()

val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()

interface UserApi {
@GET("users/{id}") suspend fun getUser(@Path("id") id: String): UserDto
@POST("users") suspend fun create(@Body req: CreateUserRequest): UserDto
@Multipart @POST("users/{id}/avatar")
suspend fun uploadAvatar(@Path("id") id: String, @Part file: MultipartBody.Part): UserDto
}
@Serializable data object Home
@Serializable data class Profile(val userId: String)

NavHost(navController, startDestination = Home) {
composable<Home> {
HomeScreen(onProfileClick = { id -> navController.navigate(Profile(id)) })
}
composable<Profile> { entry ->
val args: Profile = entry.toRoute()
ProfileScreen(userId = args.userId, onBack = navController::popBackStack)
}
}

// Pop up to start
navController.navigate(Home) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}

// Deep link
composable<Profile>(
deepLinks = listOf(navDeepLink<Profile>(basePath = "https://myapp.com/user"))
) { ... }

WorkManager cheats

@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted ctx: Context,
@Assisted params: WorkerParameters,
private val repo: Repository
) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result = try {
repo.sync()
Result.success()
} catch (e: IOException) {
Result.retry()
}
}

// One-time
val req = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(ctx).enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, req)

// Periodic
val periodic = PeriodicWorkRequestBuilder<SyncWorker>(6, TimeUnit.HOURS).build()
WorkManager.getInstance(ctx).enqueueUniquePeriodicWork(
"background_sync", ExistingPeriodicWorkPolicy.KEEP, periodic
)

// Expedited
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)

// Observe
WorkManager.getInstance(ctx).getWorkInfoByIdFlow(req.id)
.collect { info -> ... }

R8 keep rules (common)

# Models with reflection
-keep @com.squareup.moshi.JsonClass class * { *; }
-keep class * implements com.squareup.moshi.JsonAdapter { *; }

# Retrofit
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}

# Hilt
-keep,allowobfuscation @dagger.hilt.android.AndroidEntryPoint class *

# Coroutines
-keepclassmembers class kotlinx.coroutines.** { volatile <fields>; }

# Crashlytics — keep stack frames readable
-keepattributes SourceFile, LineNumberTable
-renamesourcefileattribute SourceFile

# Compose
-keep,allowobfuscation class androidx.compose.runtime.** { *; }

Gradle properties (production)

org.gradle.jvmargs=-Xmx6g -XX:+UseG1GC -XX:MaxMetaspaceSize=1g \
-Dfile.encoding=UTF-8 \
-Dkotlin.daemon.jvm.options="-Xmx4g"

org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.workers.max=8

kotlin.code.style=official
kotlin.incremental=true
kotlin.incremental.useClasspathSnapshot=true

android.useAndroidX=true
android.nonTransitiveRClass=true
android.nonFinalResIds=true
android.defaults.buildfeatures.buildconfig=false
android.defaults.buildfeatures.aidl=false

Common testing utilities

// JUnit 5
@Test fun `name with spaces`() { }
@ParameterizedTest @CsvSource("1,1", "2,4", "3,9") fun square(n: Int, sq: Int) { }

// Coroutines test
runTest { advanceTimeBy(300); runCurrent() }
val dispatcher = StandardTestDispatcher()

// Turbine
flow.test {
assertEquals(initial, awaitItem())
flow.emit(updated)
assertEquals(updated, awaitItem())
}

// MockK
val repo = mockk<Repo>()
coEvery { repo.fetch(any()) } returns user
coEvery { repo.save(any()) } throws IOException()
coVerify(exactly = 1) { repo.fetch("u1") }

// Compose
composeRule.onNodeWithText("OK").performClick()
composeRule.onNodeWithTag("submit").assertIsEnabled()
composeRule.waitUntil { composeRule.onAllNodesWithText("Loaded").fetchSemanticsNodes().isNotEmpty() }

ADB cheat sheet

# Quick references
adb devices
adb install app.apk
adb uninstall com.myapp
adb shell pm clear com.myapp
adb shell am force-stop com.myapp
adb shell am start -n com.myapp/.MainActivity
adb shell am start -a android.intent.action.VIEW -d "https://myapp.com/path"

# Logcat
adb logcat | grep "MyTag"
adb logcat *:E

# Doze testing
adb shell dumpsys deviceidle force-idle
adb shell dumpsys deviceidle unforce

# Permissions
adb shell pm grant com.myapp android.permission.CAMERA

# Screenshots / recording
adb shell screencap /sdcard/s.png && adb pull /sdcard/s.png
adb shell screenrecord /sdcard/r.mp4

# Battery
adb shell dumpsys batterystats --reset
adb bugreport bugreport.zip # for Battery Historian

# Database files
adb shell run-as com.myapp ls /data/data/com.myapp/databases/

Keyboard shortcuts (Android Studio)

ActionmacOSWindows
Search everywhere⇧⇧Shift+Shift
Recent files⌘ECtrl+E
Quick fix⌥↩Alt+Enter
Find action⌘⇧ACtrl+Shift+A
Go to declaration⌘BCtrl+B
Find usages⌥F7Alt+F7
Rename⇧F6Shift+F6
Extract method⌘⌥MCtrl+Alt+M
Reformat⌘⌥LCtrl+Alt+L
Run⌃RShift+F10
Debug⌃DShift+F9
Step overF8F8
Step intoF7F7
ResumeF9F9
File structure⌘F12Ctrl+F12
Terminal⌥F12Alt+F12