Android Cheatsheet
01

Quick Start / Setup

⚠️ Do This First β€” Without It, Nothing Works
AndroidManifest.xml β€” Internet Permission
xml
<!-- AndroidManifest.xml β€” add BEFORE the <application> tag -->
<uses-permission android:name="android.permission.INTERNET"/>

libs.versions.toml

toml
[versions]
agp = "9.2.1"
kotlin = "2.4.0"
ksp = "2.3.7"
retrofit = "3.0.0"
converterGson = "3.0.0"
loggingInterceptor = "5.3.2"
roomVersion = "2.8.4"
lifecycleRuntimeKtx = "2.10.0"
kotlinxCoroutinesAndroid = "1.11.0"

[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }

[plugins]
google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

build.gradle.kts (app module)

kotlin
plugins {
    alias(libs.plugins.google.devtools.ksp)   // required for Room
}

dependencies {
    // Retrofit + Gson converter + OkHttp logging
    implementation(libs.retrofit)
    implementation(libs.converter.gson)
    implementation(libs.logging.interceptor)

    // Room  β€” use KSP, NOT kapt!
    implementation(libs.androidx.room.runtime)
    implementation(libs.androidx.room.ktx)          // Flow + Coroutines support
    ksp(libs.androidx.room.compiler)                // annotation processor via KSP

    // ViewModel + Coroutines
    implementation(libs.androidx.lifecycle.viewmodel.compose)
    implementation(libs.kotlinx.coroutines.android)
}
πŸ’‘ KSP vs kapt: Room's annotation processor must be declared with ksp(...), not kapt(...). kapt is deprecated and will fail on modern Android projects. The KSP plugin must also be listed in the plugins { } block.
02

Retrofit Client Setup

πŸ“ ArticleDemo kotlin Β· RetroFitClient.kt
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object RetroFitClient {

    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
    // ⚠️  BASE_URL must end with a trailing slash  /

    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY   // logs full request + response in Logcat
    }

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()

    val articleApi: ArticleApi = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(ArticleApi::class.java)
}
πŸ’‘ The object keyword makes RetroFitClient a singleton β€” one shared Retrofit instance for the whole app. GsonConverterFactory automatically converts JSON ↔ Kotlin data classes.
03

API Interface & Annotations

πŸ“ ArticleDemo kotlin Β· ArticleApi.kt
interface ArticleApi {

    @GET("posts")
    suspend fun getArticles(
        @Query("_page")  page: Int  = 1,     // β†’ /posts?_page=1&_limit=20
        @Query("_limit") limit: Int = 20
    ): List<Article>

    @GET("posts/{id}")
    suspend fun getArticleById(
        @Path("id") id: Int                  // β†’ /posts/42
    ): Article

    @POST("posts")
    suspend fun createArticle(
        @Body article: Article               // serialized to JSON request body
    ): Article

    @PUT("posts/{id}")
    suspend fun updateArticle(
        @Path("id") id: Int,
        @Body article: Article
    ): Article

    @DELETE("posts/{id}")
    suspend fun deleteArticle(
        @Path("id") id: Int
    )
    // All functions are suspend β€” must be called from a coroutine scope
}

Annotation Quick Reference

AnnotationHTTP Method / PurposeExample
@GET("path")HTTP GET β€” fetch data@GET("users")
@POST("path")HTTP POST β€” create resource@POST("users")
@PUT("path/{id}")HTTP PUT β€” full replace@PUT("users/{id}")
@PATCH("path/{id}")HTTP PATCH β€” partial update@PATCH("users/{id}")
@DELETE("path/{id}")HTTP DELETE β€” remove resource@DELETE("users/{id}")
@Path("id")Replaces {id} in URL path@Path("id") id: Int
@Query("key")Appends ?key=value to URL@Query("page") page: Int
@BodySerializes object as JSON request body@Body user: User
@Header("key")Single dynamic request header@Header("Authorization") t: String
@Headers([...])Static headers on the function@Headers(["Content-Type: application/json"])
04

Header Parameters & Authentication

Single Static Header

kotlin
@Headers("Content-Type: application/json")
@GET("users")
suspend fun getUsers(): List<User>

Multiple Static Headers

kotlin
@Headers([
    "Content-Type: application/json",
    "Accept: application/json"
])
@POST("users")
suspend fun createUser(@Body user: User): User

Dynamic Authorization Header (Bearer Token)

kotlin
// Interface: dynamic header passed per call
@GET("profile")
suspend fun getProfile(
    @Header("Authorization") token: String
): UserProfile

// Call site β€” prefix with "Bearer "
val profile = api.getProfile("Bearer $accessToken")

OkHttp Interceptor β€” Inject Token on Every Request

Use this when every API call needs the same auth header, so you don't have to pass it manually each time.

kotlin Β· AuthInterceptor.kt
import okhttp3.Interceptor
import okhttp3.Response

class AuthInterceptor(private val token: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .addHeader("Content-Type", "application/json")
            .build()
        return chain.proceed(request)
    }
}
kotlin Β· RetroFitClient.kt β€” wire up the interceptor
private val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor(token = "your_token_here"))  // auth first
    .addInterceptor(loggingInterceptor)                          // logging last
    .connectTimeout(30, TimeUnit.SECONDS)
    .build()
πŸ’‘ Add the AuthInterceptor before the logging interceptor so the Authorization header appears in Logcat output for debugging.
05

DTOs & JSON Mapping

πŸ“ ArticleDemo kotlin Β· Article.kt
// Kotlin property names match JSON keys exactly β†’ no @SerializedName needed
data class Article(
    val id: Int,
    val userId: Int,
    val title: String,
    val body: String
)
// Same class is reused as both response DTO and @Body request payload

When JSON Keys Differ from Kotlin Names β†’ @SerializedName

kotlin
import com.google.gson.annotations.SerializedName

data class User(
    @SerializedName("user_id")    val id: Int,
    @SerializedName("first_name") val firstName: String,
    @SerializedName("last_name")  val lastName: String,
    @SerializedName("is_active")  val isActive: Boolean
)
// Gson maps JSON "user_id" β†’ Kotlin id, "first_name" β†’ firstName, etc.
πŸ’‘ If you forget @SerializedName and the JSON key uses snake_case (user_id) but your Kotlin property uses camelCase (userId), Gson returns null β€” no error, just silent null. Always double-check the actual JSON field names.
06

Repository & ViewModel (Network)

πŸ“ ArticleDemo kotlin Β· ArticleRepository.kt
class ArticleRepository(
    private val api: ArticleApi = RetroFitClient.articleApi
) {
    // runCatching wraps the suspend call β€” any exception becomes Result.Failure
    suspend fun getArticles(page: Int = 1, limit: Int = 20): Result<List<Article>> =
        runCatching { api.getArticles(page, limit) }

    suspend fun getArticleById(id: Int): Result<Article> =
        runCatching { api.getArticleById(id) }

    suspend fun createArticle(article: Article): Result<Article> =
        runCatching { api.createArticle(article) }

    suspend fun updateArticle(id: Int, article: Article): Result<Article> =
        runCatching { api.updateArticle(id, article) }

    suspend fun deleteArticle(id: Int): Result<Unit> =
        runCatching { api.deleteArticle(id) }
}
πŸ“ ArticleDemo kotlin Β· ArticleUiState.kt
sealed class ArticleUiState {
    data object Loading                          : ArticleUiState()
    data class  Success(val articles: List<Article>) : ArticleUiState()
    data class  Error(val message: String)       : ArticleUiState()
}
πŸ“ ArticleDemo kotlin Β· ArticleViewModel.kt
class ArticleViewModel(
    private val repository: ArticleRepository = ArticleRepository()
) : ViewModel() {

    private val _uiState = MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)
    val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()

    init { loadArticles() }

    fun loadArticles() {
        viewModelScope.launch {
            _uiState.value = ArticleUiState.Loading
            repository.getArticles()
                .onSuccess { articles -> _uiState.value = ArticleUiState.Success(articles) }
                .onFailure { error   -> _uiState.value = ArticleUiState.Error(error.message ?: "Unknown error") }
        }
    }
}

Collecting StateFlow in Compose

kotlin Β· Composable screen
val uiState by viewModel.uiState.collectAsState()

when (val state = uiState) {
    is ArticleUiState.Loading -> CircularProgressIndicator()
    is ArticleUiState.Success -> LazyColumn { items(state.articles) { ArticleCard(it) } }
    is ArticleUiState.Error   -> Text("Error: ${state.message}", color = Color.Red)
}
07

Reacting to HTTP Status Codes

Change the return type from T to Response<T> to inspect HTTP status codes directly instead of relying on exceptions.

kotlin
// Interface: wrap return type in Response<T>
interface ArticleApi {
    @GET("posts/{id}")
    suspend fun getArticleById(@Path("id") id: Int): Response<Article>
}

// Repository / ViewModel β€” inspect status manually
val response = api.getArticleById(id)

if (response.isSuccessful) {
    val article = response.body()          // nullable β€” the deserialized body
    // handle success ...
} else {
    val errorBody = response.errorBody()?.string()
    when (response.code()) {
        400 -> { /* Bad Request β€” invalid input sent */ }
        401 -> { /* Unauthorized β€” token missing or expired, redirect to login */ }
        403 -> { /* Forbidden β€” valid token but no permission */ }
        404 -> { /* Not Found β€” resource does not exist */ }
        500 -> { /* Internal Server Error β€” backend crash */ }
    }
}

HTTP Status Code Reference

CodeNameMeaningAction
200OKSuccess with bodyUse response.body()
201CreatedResource was createdUse response.body() for the created item
204No ContentSuccess, empty bodyDon't call .body() β€” it returns null
400Bad RequestInvalid input from clientShow validation error to user
401UnauthorizedMissing or expired tokenRedirect to login screen
403ForbiddenValid token, but no permissionShow "Access denied" message
404Not FoundResource doesn't existShow "Not found" UI state
500Internal Server ErrorBackend crashedShow "Try again later" + log
⚑ runCatching vs Response<T>: runCatching { } catches network exceptions (no internet, timeout, DNS failure) but treats every HTTP response β€” including 4xx/5xx β€” as success from its perspective. Response<T> lets you inspect the HTTP status code. Combine both for full coverage: wrap the Response<T> call in runCatching too.
08

Room Database

@Entity β€” data class mapped to a DB table

πŸ“ VotingApp kotlin Β· Party.kt
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "parties")
data class Party(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val name: String,
    val votes: Int = 0
)

@Dao β€” database access interface

πŸ“ VotingApp kotlin Β· PartyDao.kt
import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface PartyDao {

    // Flow = non-suspend, reactive β€” Room emits new list whenever DB changes
    @Query("SELECT * FROM parties ORDER BY name ASC")
    fun getAllParties(): Flow<List<Party>>

    @Insert
    suspend fun insert(party: Party)

    // Custom SQL UPDATE via @Query (more flexible than @Update)
    @Query("UPDATE parties SET votes = votes + :amount WHERE id = :partyId")
    suspend fun updateVotes(partyId: Int, amount: Int)

    @Query("UPDATE parties SET votes = 0 WHERE id = :partyId")
    suspend fun resetVotes(partyId: Int)

    // Standard convenience annotations also available:
    // @Update  suspend fun update(party: Party)
    // @Delete  suspend fun delete(party: Party)
}

@Database β€” singleton room database

πŸ“ VotingApp kotlin Β· VotingDatabase.kt
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase

@Database(entities = [Party::class], version = 1)
abstract class VotingDatabase : RoomDatabase() {

    abstract fun partyDao(): PartyDao

    companion object {
        @Volatile                            // ensures visibility across threads
        private var INSTANCE: VotingDatabase? = null

        fun get(context: Context): VotingDatabase =
            INSTANCE ?: synchronized(this) { // double-checked locking for thread safety
                INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,
                    VotingDatabase::class.java,
                    "voting.db"
                )
                .addCallback(object : RoomDatabase.Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)
                        // Seed data β€” runs only on first database creation
                        db.execSQL("INSERT INTO parties (name, votes) VALUES ('Party A', 0)")
                        db.execSQL("INSERT INTO parties (name, votes) VALUES ('Party B', 0)")
                    }
                })
                .build()
                .also { INSTANCE = it }
            }
    }
}

ViewModel β€” Flow β†’ StateFlow pattern

πŸ“ VotingApp kotlin Β· CounterViewModel.kt
class CounterViewModel(private val repository: VotingRepository) : ViewModel() {

    // Convert DAO Flow into a StateFlow the UI can collect
    val uiState: StateFlow<CountUiState> = repository.getAllParties()
        .map { parties -> CountUiState(parties = parties, isLoading = false) }
        .stateIn(
            scope   = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),   // 5s grace before stopping
            initialValue = CountUiState(isLoading = true)
        )

    fun addVote(partyId: Int) {
        viewModelScope.launch { repository.updateVotes(partyId, 1) }
    }

    fun removeVote(partyId: Int) {
        viewModelScope.launch { repository.updateVotes(partyId, -1) }
    }

    // ViewModelProvider.Factory β€” inject dependencies without Hilt/Koin
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val app = this[APPLICATION_KEY]!!
                val dao = VotingDatabase.get(app).partyDao()
                CounterViewModel(VotingRepositoryImpl(dao))
            }
        }
    }
}
πŸ’‘ Rule of thumb: DAO reads return Flow<T> (non-suspend, reactive stream). DAO writes are suspend fun. Never call suspend functions from the main thread outside a coroutine scope β€” it will crash.
09

Room + Retrofit: Offline-First Pattern

The UI always observes Room's Flow as the single source of truth. Retrofit only refreshes the data in the background. If the network fails, the cached Room data stays visible.

kotlin Β· ArticleRepository.kt (offline-first)
class ArticleRepository(
    private val api: ArticleApi,
    private val dao: ArticleDao
) {
    // UI observes this Flow β€” Room auto-pushes updates whenever data changes
    fun getArticles(): Flow<List<Article>> = dao.getAllArticles()

    // Called from viewModelScope.launch β€” refreshes cache from network
    suspend fun refreshArticles() {
        runCatching { api.getArticles() }
            .onSuccess { articles ->
                dao.deleteAll()          // clear stale data
                dao.insertAll(articles)  // insert fresh data from network
            }
        // On failure: Room data remains intact, UI continues working offline
    }
}
kotlin Β· ViewModel
class ArticleViewModel(private val repository: ArticleRepository) : ViewModel() {

    // Always backed by Room β€” survives network failures
    val articles: StateFlow<List<Article>> = repository.getArticles()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    init {
        // Kick off a background network refresh on startup
        viewModelScope.launch { repository.refreshArticles() }
    }
}
10

Gotchas, Quick Tables & Reference

Common Mistakes

  • ❌Forgot <uses-permission android:name="android.permission.INTERNET"/> in AndroidManifest.xml β†’ silent network failure or crash
  • ❌BASE_URL does not end with / β†’ Retrofit path resolution silently breaks
  • ❌Used kapt(libs.androidx.room.compiler) instead of ksp(...) β†’ build error on modern Android projects
  • ❌Called a suspend fun from the main thread without a coroutine scope β†’ app crashes with IllegalStateException
  • ❌Typo in Room @Query column or table name β†’ runtime crash (NOT a compile error)
  • ❌Missing @SerializedName when JSON key uses snake_case but Kotlin property uses camelCase β†’ Gson silently returns null
  • ❌Returning T from the API interface instead of Response<T> when you need to check HTTP status codes
  • ❌Making a DAO read function suspend instead of returning Flow<T> β€” it becomes a one-shot call instead of a reactive stream

Flow vs suspend β€” When to Use Each

Use CaseReturn TypeNotes
Read data that should auto-update the UIFlow<List<T>>DAO reads; not suspend
Write / Insert / Update / Delete in DBsuspend funDAO writes
One-shot network requestsuspend funAll Retrofit calls
Network call where you need HTTP codesuspend fun : Response<T>Check isSuccessful + .code()
Error-safe network callResult<T> via runCatchingUse onSuccess / onFailure

StateFlow Quick Reference

kotlin
// Declare in ViewModel
private val _state = MutableStateFlow<MyState>(MyState.Loading)
val state: StateFlow<MyState> = _state.asStateFlow()   // expose read-only

// Update from ViewModel
_state.value = MyState.Success(data)

// Collect in Compose
val s by viewModel.state.collectAsState()

// Convert Room Flow to StateFlow
val list = repository.getItems()
    .stateIn(
        scope        = viewModelScope,
        started      = SharingStarted.WhileSubscribed(5_000),
        initialValue = emptyList()
    )

Retrofit Annotations Cheat Card

AnnotationWhereEffect
@GET / @POST / @PUT / @PATCH / @DELETEOn functionHTTP verb + relative URL
@Path("x")ParameterReplaces {x} in URL
@Query("k")ParameterAppends ?k=value to URL
@BodyParameterSerializes as JSON request body
@Header("k")ParameterAdds header dynamically per call
@Headers([...])On functionStatic headers baked into the call
@FormUrlEncoded + @FieldFunction + paramSends application/x-www-form-urlencoded body
@Multipart + @PartFunction + paramSends multipart/form-data (file upload)

Room Annotations Cheat Card

AnnotationWhereEffect
@Entity(tableName = "...")data classMaps class to a DB table
@PrimaryKey(autoGenerate = true)PropertyAuto-incrementing primary key
@ColumnInfo(name = "...")PropertyCustom column name (like @SerializedName but for DB)
@IgnorePropertyRoom ignores this field (not stored in DB)
@DaointerfaceMarks as DAO β€” Room generates the implementation
@Insertsuspend funInsert one or many entities
@Updatesuspend funUpdate by primary key match
@Deletesuspend funDelete by primary key match
@Query("SQL")fun / suspend funCustom SQL; non-suspend + Flow for reactive reads
@Database(entities=[...], version=N)abstract classDefines the database schema and version