🚀 Jetpack Compose Cheat Sheet

Composables, State, Navigation, ViewModel, side effects and Material 3.

🔍
🧩 Composables
Basic Composable
@Composable
fun Greeting(name: String) {
  Text(
    text = "Hello, $name!",
    style = MaterialTheme.typography.titleLarge,
    color = MaterialTheme.colorScheme.primary
  )
}

// Composable with modifier (best practice)
@Composable
fun UserCard(
  user: User,
  modifier: Modifier = Modifier,
  onClick: () -> Unit = {}
) {
  Card(
    modifier = modifier
      .fillMaxWidth()
      .clickable(onClick = onClick),
    elevation = CardDefaults.cardElevation(4.dp)
  ) {
    Column(Modifier.padding(16.dp)) {
      Text(user.name, style = MaterialTheme.typography.titleMedium)
      Text(user.email, style = MaterialTheme.typography.bodySmall)
    }
  }
}
Common components
// Text
Text("Hello", style = MaterialTheme.typography.bodyLarge)

// Button
Button(onClick = { /* action */ }) { Text("Click") }
OutlinedButton(onClick = {}) { Text("Outlined") }
TextButton(onClick = {}) { Text("Text") }
IconButton(onClick = {}) { Icon(Icons.Default.Add, null) }
FloatingActionButton(onClick = {}) { Icon(Icons.Default.Add, null) }

// TextField
var text by remember { mutableStateOf("") }
OutlinedTextField(
  value = text,
  onValueChange = { text = it },
  label = { Text("Email") },
  leadingIcon = { Icon(Icons.Default.Email, null) },
  isError = text.isEmpty(),
  keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
  singleLine = true,
  modifier = Modifier.fillMaxWidth()
)

// Checkbox, Switch, RadioButton
var checked by remember { mutableStateOf(false) }
Checkbox(checked = checked, onCheckedChange = { checked = it })
Switch(checked = checked, onCheckedChange = { checked = it })

// Image
AsyncImage(    // Coil library
  model = "https://...",
  contentDescription = null,
  contentScale = ContentScale.Crop,
  modifier = Modifier.size(80.dp).clip(CircleShape)
)
🔄 State Management
remember & mutableStateOf
// Basic state
var count by remember { mutableStateOf(0) }
var text  by remember { mutableStateOf("") }
var list  by remember { mutableStateOf(listOf<Item>()) }

// Remember with key — recomputes when key changes
val processed by remember(userId) { mutableStateOf(process(userId)) }

// rememberSaveable — survives rotation
var input by rememberSaveable { mutableStateOf("") }

// Derived state — recomputes only when dependencies change
val isValid by remember { derivedStateOf { text.isNotEmpty() } }

// Snapshot state list/map
val items = remember { mutableStateListOf<Item>() }
val map   = remember { mutableStateMapOf<String, Int>() }
items.add(newItem)    // triggers recomposition
State hoisting pattern
// Stateful (owns state)
@Composable
fun CounterScreen() {
  var count by remember { mutableStateOf(0) }
  CounterContent(
    count = count,
    onIncrement = { count++ },
    onDecrement = { count-- }
  )
}

// Stateless (receives state + callbacks)
@Composable
fun CounterContent(
  count: Int,
  onIncrement: () -> Unit,
  onDecrement: () -> Unit,
  modifier: Modifier = Modifier
) {
  Row(modifier) {
    IconButton(onClick = onDecrement) { Icon(Icons.Default.Remove, null) }
    Text("$count")
    IconButton(onClick = onIncrement) { Icon(Icons.Default.Add, null) }
  }
}
📐 Layout
Column, Row, Box
Column(
  modifier = Modifier.fillMaxSize().padding(16.dp),
  verticalArrangement = Arrangement.spacedBy(8.dp),
  horizontalAlignment = Alignment.CenterHorizontally
) {
  Text("Top")
  Spacer(Modifier.height(8.dp))
  Text("Bottom")
}

Row(
  modifier = Modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceBetween,
  verticalAlignment = Alignment.CenterVertically
) {
  Icon(Icons.Default.Star, null)
  Text("Title", Modifier.weight(1f))    // weight = Expanded equivalent
  Text("Right")
}

Box(Modifier.fillMaxSize()) {
  Image(...)
  Text("Overlay", Modifier.align(Alignment.BottomCenter).padding(8.dp))
}
Modifier chaining
Modifier
  .fillMaxWidth()
  .fillMaxHeight()         // or .fillMaxSize()
  .size(100.dp)
  .width(200.dp).height(50.dp)
  .padding(16.dp)
  .padding(horizontal = 16.dp, vertical = 8.dp)
  .background(MaterialTheme.colorScheme.surface)
  .clip(RoundedCornerShape(12.dp))
  .border(1.dp, Color.Gray, RoundedCornerShape(12.dp))
  .clickable { }
  .alpha(0.5f)
  .offset(x = 8.dp, y = 4.dp)
  .wrapContentSize()
  .statusBarsPadding()
  .navigationBarsPadding()
  .imePadding()            // avoid keyboard
Scaffold & TopAppBar
Scaffold(
  topBar = {
    TopAppBar(
      title = { Text("Home") },
      navigationIcon = {
        IconButton(onClick = { navController.navigateUp() }) {
          Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
        }
      },
      actions = {
        IconButton(onClick = { }) {
          Icon(Icons.Default.Search, null)
        }
      },
      colors = TopAppBarDefaults.topAppBarColors(
        containerColor = MaterialTheme.colorScheme.primaryContainer
      )
    )
  },
  floatingActionButton = {
    FloatingActionButton(onClick = { }) {
      Icon(Icons.Default.Add, null)
    }
  },
  bottomBar = { BottomNavBar(navController) }
) { padding ->
  // Content — always apply the padding!
  Column(Modifier.padding(padding)) { ... }
}
📋 Lazy Lists
LazyColumn & LazyRow
LazyColumn(
  modifier = Modifier.fillMaxSize(),
  contentPadding = PaddingValues(16.dp),
  verticalArrangement = Arrangement.spacedBy(8.dp),
  state = listState   // LazyListState for scroll control
) {
  // Header
  item { Text("Header", style = MaterialTheme.typography.titleLarge) }

  // Section with sticky header
  stickyHeader { SectionHeader("Recent") }

  // Items
  items(items = items, key = { it.id }) { item ->
    ItemCard(item = item)
  }

  // Items with index
  itemsIndexed(items) { index, item ->
    if (index > 0) Divider()
    ItemRow(item = item)
  }

  // Footer / load more
  item {
    if (isLoadingMore) CircularProgressIndicator()
  }
}

// LazyRow (horizontal)
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
  items(categories) { cat -> ChipButton(cat) }
}

// Scroll state
val listState = rememberLazyListState()
val isAtBottom by remember {
  derivedStateOf {
    val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()
    last?.index == items.lastIndex
  }
}
LazyVerticalGrid
LazyVerticalGrid(
  columns = GridCells.Adaptive(minSize = 160.dp),
  contentPadding = PaddingValues(16.dp),
  horizontalArrangement = Arrangement.spacedBy(8.dp),
  verticalArrangement = Arrangement.spacedBy(8.dp)
) {
  items(photos, key = { it.id }) { photo ->
    PhotoCard(photo = photo)
  }
}
🧠 ViewModel
ViewModel with StateFlow
class HomeViewModel(
  private val getUsersUseCase: GetUsersUseCase,
  private val savedStateHandle: SavedStateHandle
) : ViewModel() {

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

  private val _events = MutableSharedFlow<HomeEvent>()
  val events: SharedFlow<HomeEvent> = _events.asSharedFlow()

  init { loadUsers() }

  fun loadUsers() {
    viewModelScope.launch {
      _uiState.value = HomeUiState.Loading
      getUsersUseCase()
        .onSuccess { _uiState.value = HomeUiState.Success(it) }
        .onFailure { _uiState.value = HomeUiState.Error(it.message ?: "") }
    }
  }

  fun onUserClick(user: User) {
    viewModelScope.launch {
      _events.emit(HomeEvent.NavigateTo(Routes.detail(user.id)))
    }
  }
}

// Composable
@Composable
fun HomeScreen(navController: NavController) {
  val vm: HomeViewModel = hiltViewModel()
  val uiState by vm.uiState.collectAsStateWithLifecycle()

  LaunchedEffect(Unit) {
    vm.events.collect { event ->
      when (event) {
        is HomeEvent.NavigateTo -> navController.navigate(event.route)
      }
    }
  }

  when (uiState) {
    is HomeUiState.Loading -> LoadingScreen()
    is HomeUiState.Success -> UserList((uiState as HomeUiState.Success).users, vm::onUserClick)
    is HomeUiState.Error   -> ErrorScreen((uiState as HomeUiState.Error).message)
  }
}
⚡ Side Effects
LaunchedEffect, SideEffect, DisposableEffect
// LaunchedEffect — coroutine, re-runs when keys change
LaunchedEffect(Unit) { /* one-time setup */ }
LaunchedEffect(userId) { vm.loadUser(userId) }
LaunchedEffect(snackbarState) { /* collect events */ }

// SideEffect — runs after every successful recomposition
SideEffect {
  systemUiController.setStatusBarColor(color)
}

// DisposableEffect — cleanup on leave
DisposableEffect(lifecycleOwner) {
  val observer = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_RESUME) vm.refresh()
  }
  lifecycleOwner.lifecycle.addObserver(observer)
  onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}

// rememberCoroutineScope — for non-composable callbacks
val scope = rememberCoroutineScope()
Button(onClick = {
  scope.launch { scrollState.animateScrollTo(0) }
}) { Text("Scroll to top") }
✨ Animation
Animate state changes
// animateFloatAsState
val alpha by animateFloatAsState(
  targetValue = if (isVisible) 1f else 0f,
  animationSpec = tween(300),
  label = "alpha"
)
Box(Modifier.alpha(alpha)) { ... }

// animateDpAsState
val size by animateDpAsState(
  targetValue = if (selected) 120.dp else 80.dp,
  animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
  label = "size"
)

// animateColorAsState
val color by animateColorAsState(
  targetValue = if (selected) MaterialTheme.colorScheme.primary else Color.Gray,
  label = "color"
)

// AnimatedVisibility
AnimatedVisibility(
  visible = isVisible,
  enter = fadeIn() + slideInVertically(),
  exit  = fadeOut() + slideOutVertically()
) { Content() }

// AnimatedContent
AnimatedContent(targetState = count, label = "count") { target ->
  Text("$target")
}
🎨 Material 3 Theme
M3 Theme access
// Colors MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.secondary MaterialTheme.colorScheme.surface MaterialTheme.colorScheme.background MaterialTheme.colorScheme.onPrimary MaterialTheme.colorScheme.primaryContainer MaterialTheme.colorScheme.error // Typography MaterialTheme.typography.displayLarge MaterialTheme.typography.titleLarge MaterialTheme.typography.titleMedium MaterialTheme.typography.bodyLarge MaterialTheme.typography.bodyMedium MaterialTheme.typography.labelSmall // Shapes MaterialTheme.shapes.small // 4dp MaterialTheme.shapes.medium // 8dp MaterialTheme.shapes.large // 16dp MaterialTheme.shapes.extraLarge // 28dp
Dynamic color theme
@Composable
fun AppTheme(
  darkTheme: Boolean = isSystemInDarkTheme(),
  dynamicColor: Boolean = true,
  content: @Composable () -> Unit
) {
  val colorScheme = when {
    dynamicColor && Build.VERSION.SDK_INT >= 31 -> {
      if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
      else dynamicLightColorScheme(LocalContext.current)
    }
    darkTheme -> DarkColorScheme
    else -> LightColorScheme
  }

  MaterialTheme(
    colorScheme = colorScheme,
    typography = AppTypography,
    content = content
  )
}
⚡ Pro Tips
Performance best practices
// Use keys in LazyColumn to avoid full recompose
items(list, key = { it.id }) { ItemCard(it) }

// Don't read state in parent if only child needs it
// Bad: triggers parent recompose
Column { Text("$count") }  // reads count in Column scope
// Good: isolate to where used
Column { CountText(count) }  // only CountText recomposes

// @Stable for stable classes (skip recompose if equal)
@Stable
data class User(val id: String, val name: String)

// Avoid lambda allocations — use stable references
val onClick = remember { { item: Item -> handleClick(item) } }

// Prefer collectAsStateWithLifecycle (lifecycle-aware)
val state by vm.uiState.collectAsStateWithLifecycle()

// Use key() to force reset
key(userId) { UserProfile(userId) }   // fresh instance on userId change
Hilt + Compose
// ViewModel injection val vm: HomeViewModel = hiltViewModel() // Scoped to nav entry val vm: HomeViewModel = hiltViewModel( viewModelStoreOwner = navController.getBackStackEntry(Routes.HOME) ) // Entry points in composables (if needed) @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { AppNavGraph() } } } }