Careeroza
State, Lists and Navigation in Compose
remember and mutableStateOf
mutableStateOf creates observable state: when it changes, composables that read it recompose. remember keeps it across recompositions; rememberSaveable also survives rotation.
@Composable
fun QuantityStepper(modifier: Modifier = Modifier) {
var qty by rememberSaveable { mutableIntStateOf(1) }
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { if (qty > 1) qty-- }) { Icon(Icons.Default.Remove, contentDescription = "Less") }
Text("$qty", style = MaterialTheme.typography.titleMedium)
IconButton(onClick = { qty++ }) { Icon(Icons.Default.Add, contentDescription = "More") }
}
}
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) { // hoisted state
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
label = { Text("Search products") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
}LazyColumn and Screen States
@Composable
fun ProductsScreen(state: ProductsUiState, onRetry: () -> Unit, onAdd: (Product) -> Unit) {
when (state) {
ProductsUiState.Loading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
is ProductsUiState.Error -> Column(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(state.message)
Button(onClick = onRetry) { Text("Retry") }
}
is ProductsUiState.Success -> LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(state.products, key = { it.id }) { product ->
ProductCard(product, onAdd = { onAdd(product) })
}
}
}
}Tip: Always give items a stable key (the database id). Compose then keeps scroll position and animations correct when the list changes.
Type-safe Navigation
Navigation Compose 2.8+ uses @Serializable classes as routes, so arguments are type-checked.
@Serializable object ProductList
@Serializable data class ProductDetail(val productId: Long)
@Composable
fun ShopNavHost(modifier: Modifier = Modifier, navController: NavHostController = rememberNavController()) {
NavHost(navController = navController, startDestination = ProductList, modifier = modifier) {
composable<ProductList> {
ProductsRoute(onOpen = { id -> navController.navigate(ProductDetail(id)) })
}
composable<ProductDetail> { backStackEntry ->
val args = backStackEntry.toRoute<ProductDetail>()
ProductDetailRoute(productId = args.productId, onBack = { navController.popBackStack() })
}
}
}Side effects that must run when a composable appears use LaunchedEffect; it runs in a coroutine and is cancelled when the composable leaves:
LaunchedEffect(productId) { viewModel.load(productId) } // re-runs if productId changes
val scope = rememberCoroutineScope()
val snackbar = remember { SnackbarHostState() }
Button(onClick = { scope.launch { snackbar.showSnackbar("Added to cart") } }) { Text("Add") }