# Compose State Management Patterns Visual guide to state management in Compose Multiplatform. For Kotlin-specific patterns (StateFlow, sealed classes), see `kotlin-expert` skill. ## Core State Functions ### remember Cache values across recompositions: ```kotlin @Composable fun Counter() { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times") } } ``` **When to use**: Simple UI state (toggles, counters, text input) **Visual pattern**: Button press → state changes → UI updates ### derivedStateOf Compute state from other state, recompose only when result changes: ```kotlin @Composable fun ScrollToTopButton(listState: LazyListState) { // Only recomposes when showButton value changes (not every scroll pixel) val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } if (showButton) { FloatingActionButton(onClick = { /* scroll to top */ }) { Icon(Icons.Default.ArrowUpward, null) } } } ``` **When to use**: Input state changes frequently, but derived result changes rarely **Visual pattern**: Scroll position (0, 1, 2...) → boolean (show/hide) → FAB visibility **Performance**: Prevents recomposition on every scroll event ### produceState Convert non-Compose state into Compose state: ```kotlin @Composable fun LoadUserProfile(userId: String): State { return produceState(initialValue = null, userId) { value = repository.fetchUser(userId) } } @Composable fun ProfileScreen(userId: String) { val user by LoadUserProfile(userId) when (user) { null -> LoadingState("Loading profile...") else -> ProfileCard(user!!) } } ``` **When to use**: Convert Flow, LiveData, callbacks into Compose state **Visual pattern**: Async operation → state updates → UI reflects changes **Lifecycle**: Coroutine cancelled when composable leaves composition ## State Hoisting Pattern Move state up to make composables reusable and testable: ### Before (Stateful) ```kotlin @Composable fun SearchBar() { var query by remember { mutableStateOf("") } TextField( value = query, onValueChange = { query = it }, placeholder = { Text("Search...") } ) } ``` ❌ Hard to test, can't control state externally ### After (Stateless) ```kotlin @Composable fun SearchBar( query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier ) { TextField( value = query, onValueChange = onQueryChange, placeholder = { Text("Search...") }, modifier = modifier ) } @Composable fun SearchScreen() { var query by remember { mutableStateOf("") } Column { SearchBar(query = query, onQueryChange = { query = it }) SearchResults(query = query) } } ``` ✅ Reusable, testable, state controlled by parent **Hoisting principle**: State goes up, events go down - State: `query: String` (read-only) - Events: `onQueryChange: (String) -> Unit` (write-only) ## Amethyst State Patterns ### Theme-Aware State ```kotlin @Composable private fun isLightTheme(): Boolean { val background = MaterialTheme.colorScheme.background return (background.red + background.green + background.blue) / 3 > 0.5f } @Composable fun ThemedContent() { val isDark = !isLightTheme() // Adjust visuals based on theme val iconTint = if (isDark) Color.White else Color.Black } ``` **Pattern**: Derive state from MaterialTheme **Visual**: Component adapts to light/dark theme automatically ### Relay Status State ```kotlin @Composable fun RelayStatusIndicator( connectedCount: Int, onRefresh: () -> Unit, modifier: Modifier = Modifier ) { val statusColor = when { connectedCount == 0 -> RelayStatusColors.Disconnected connectedCount < 3 -> RelayStatusColors.Connecting else -> RelayStatusColors.Connected } Icon( imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close, tint = statusColor ) } ``` **Pattern**: Visual state derived from domain state **Visual mapping**: - 0 relays → Red + X icon - 1-2 relays → Yellow + Check icon - 3+ relays → Green + Check icon ### Loading/Empty/Error States ```kotlin @Composable fun FeedScreen(viewModel: FeedViewModel) { val uiState by viewModel.uiState.collectAsState() when (uiState) { is UiState.Loading -> LoadingState("Loading feed...") is UiState.Empty -> FeedEmptyState(onRefresh = { viewModel.refresh() }) is UiState.Error -> FeedErrorState( errorMessage = uiState.message, onRetry = { viewModel.retry() } ) is UiState.Success -> LazyColumn { items(uiState.items) { FeedItem(it) } } } } ``` **Pattern**: Sealed class → visual state component **Components**: - `LoadingState` - Progress indicator - `EmptyState` - Empty message + refresh - `ErrorState` - Error message + retry - Success - Actual content ## Common Patterns ### Toggle State ```kotlin var isExpanded by remember { mutableStateOf(false) } IconButton(onClick = { isExpanded = !isExpanded }) { Icon( if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, contentDescription = if (isExpanded) "Collapse" else "Expand" ) } if (isExpanded) { Text("Expanded content...") } ``` ### List State with Actions ```kotlin var items by remember { mutableStateOf(listOf("Item 1", "Item 2")) } Column { AddButton(onClick = { items = items + "Item ${items.size + 1}" }) items.forEachIndexed { index, item -> Row { Text(item) RemoveButton(onClick = { items = items.filterIndexed { i, _ -> i != index } }) } } } ``` ### TextField State ```kotlin var text by remember { mutableStateOf("") } TextField( value = text, onValueChange = { text = it }, label = { Text("Enter text") } ) ``` ## Performance Patterns ### Avoid Unnecessary Recomposition ```kotlin // ❌ Bad: Recomposes on every scroll position change @Composable fun BadScrollButton(scrollState: ScrollState) { if (scrollState.value > 100) { // scrollState.value changes constantly Button(onClick = { /* ... */ }) { Text("Scroll to Top") } } } // ✅ Good: Only recomposes when visibility changes @Composable fun GoodScrollButton(scrollState: ScrollState) { val showButton by remember { derivedStateOf { scrollState.value > 100 } } if (showButton) { Button(onClick = { /* ... */ }) { Text("Scroll to Top") } } } ``` ### Stable Parameters Use `@Immutable` data classes (see `kotlin-expert`) to prevent recomposition: ```kotlin @Immutable data class UserProfile(val name: String, val avatar: String) @Composable fun ProfileCard(profile: UserProfile) { // Only recomposes when profile instance changes Row { RobohashImage(robot = profile.avatar) Text(profile.name) } } ``` ## Integration with Kotlin State For ViewModel state, Flow, StateFlow → See `kotlin-expert` skill Common integration pattern: ```kotlin // ViewModel (Kotlin state) class FeedViewModel { private val _uiState = MutableStateFlow(UiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() } // Composable (Compose state) @Composable fun FeedScreen(viewModel: FeedViewModel) { val uiState by viewModel.uiState.collectAsState() // Use uiState to render UI } ``` ## Quick Reference | Function | Use Case | Recomposes When | |----------|----------|----------------| | `remember { mutableStateOf() }` | Local UI state | State value changes | | `derivedStateOf { }` | Computed state | Derived result changes | | `produceState { }` | Async/Flow → State | Async operation updates value | | `collectAsState()` | Flow → State | Flow emits new value | | State hoisting | Reusable components | Parent passes new state | ## Sources State management patterns based on: - [State and Jetpack Compose - Android Developers](https://developer.android.com/develop/ui/compose/state) - [When should I use derivedStateOf?](https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b) - [Advanced State and Side Effects](https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects) - AmethystMultiplatform codebase patterns (2025)