remember and mutableStateOf
Why State Matters in Compose
In Compose, the UI is a function of state. When state changes, the UI recomposes. Without state, nothing changes on screen. The core state APIs are remember and mutableStateOf.
Creating State
@Composable
fun Counter() {
// remember preserves the value across recompositions
// mutableStateOf creates an observable state holder
var count by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Count: $count", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
remember stores the value in the composition's slot table. It survives recomposition but is lost on configuration change (rotation, etc.). For that, use rememberSaveable.
remember vs rememberSaveable
// Survives recomposition, lost on process death / config change
var text by remember { mutableStateOf("") }
// Survives both recomposition and config change
var text by rememberSaveable { mutableStateOf("") }
rememberSaveable uses a Bundle under the hood, so it only works with types that can be saved to a Bundle (primitives, Strings, Serializable, Parcelable, or custom Saver objects).
State Types in Compose
| API | Survives Recomposition | Survives Config Change | Observable |
|---|---|---|---|
mutableStateOf |
No | No | Yes |
remember { mutableStateOf } |
Yes | No | Yes |
rememberSaveable { mutableStateOf } |
Yes | Yes | Yes |
mutableStateListOf |
Yes | No | Yes |
List State
For observable lists, use mutableStateListOf:
@Composable
fun TodoList() {
var items = remember { mutableStateListOf<TodoItem>() }
Column {
items.forEach { item ->
TodoRow(item = item, onRemove = { items.remove(item) })
}
}
}
Delegated State
Kotlin property delegation (by) makes state read/write more natural:
// Without delegation - verbose
val countState = remember { mutableIntStateOf(0) }
Text(countState.intValue.toString())
Button(onClick = { countState.intValue++ }) { }
// With delegation - clean
var count by remember { mutableIntStateOf(0) }
Text(count.toString())
Button(onClick = { count++ }) { }
State Hoisting
What Is State Hoisting?
State hoisting is the pattern of moving state up from a child composable to its parent. The child becomes stateless: it receives state as a parameter and emits events upward. This makes composables reusable, testable, and easier to reason about.
Before Hoisting
// Stateful - has internal state, hard to reuse or test
@Composable
fun NameInput() {
var name by remember { mutableStateOf("") }
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Enter name") }
)
}
After Hoisting
// Stateless - receives state and events from parent
@Composable
fun NameInput(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = value,
onValueChange = onValueChange,
label = { Text("Enter name") },
modifier = modifier
)
}
// Parent controls the state
@Composable
fun GreetingScreen() {
var name by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
NameInput(value = name, onValueChange = { name = it })
if (name.isNotEmpty()) {
Text(text = "Hello, $name!")
}
}
}
The Hoisting Rule
If a composable has state, ask:
- Is this state needed by the parent? If yes, hoist it.
- Is this state needed across recompositions? Use
remember. - Is this state needed across config changes? Use
rememberSaveable. - Should the state survive navigation? Use a ViewModel.
Single Source of Truth
State hoisting enforces a single source of truth. Instead of multiple composables each holding their own copy of data, one parent owns the state and distributes it down. This prevents inconsistencies.
// BAD: Multiple sources of truth
@Composable
fun FormScreen() {
Column {
EmailInput() // Each holds its own state
EmailConfirm() // No way to compare them
}
}
// GOOD: Single source of truth
@Composable
fun FormScreen() {
var email by remember { mutableStateOf("") }
Column {
EmailInput(value = email, onValueChange = { email = it })
EmailConfirm(value = email, onValueChange = { /* validate */ })
}
}
Hoisting Patterns by Complexity
Simple state: remember + mutableStateOf at the parent level.
Complex state: Use a state holder class that encapsulates related state and logic.
// State holder groups related state and actions
class FormState {
var email by mutableStateOf("")
private set
var password by mutableStateOf("")
private set
val isValid: Boolean get() = email.contains("@") && password.length >= 8
fun updateEmail(newEmail: String) { email = newEmail }
fun updatePassword(newPassword: String) { password = newPassword }
}
@Composable
fun rememberFormState(): FormState {
return remember { FormState() }
}
@Composable
fun LoginScreen() {
val state = rememberFormState()
Column(modifier = Modifier.padding(16.dp)) {
EmailInput(value = state.email, onValueChange = state::updateEmail)
PasswordInput(value = state.password, onValueChange = state::updatePassword)
Button(
onClick = { /* submit */ },
enabled = state.isValid
) {
Text("Log In")
}
}
}
This keeps the composable tree clean while centralizing business logic.
Derived State and StateFlow
derivedStateOf
Sometimes you have one piece of state that derives from another. Recomputing derived values on every recomposition wastes work. derivedStateOf caches the computation and only triggers recomposition when the result actually changes.
@Composable
fun FilteredList(items: List<String>, query: String) {
val filtered by remember(items, query) {
derivedStateOf {
if (query.isEmpty()) items
else items.filter { it.contains(query, ignoreCase = true) }
}
}
LazyColumn {
items(filtered) { Text(it, modifier = Modifier.padding(16.dp)) }
}
}
Without derivedStateOf, the filter runs on every recomposition. With it, the filter only runs when items or query changes, and the list UI only recomposes when the filtered result differs.
When to Use derivedStateOf
Use it when:
- A derived value changes less frequently than the state it reads.
- You want to avoid unnecessary recompositions.
- The computation is expensive.
Do not use it when:
- The derived value changes as often as the source (just read the state directly).
- The computation is trivial (just a property access).
StateFlow and collectAsStateWithLifecycle
In production apps, state often lives in ViewModels as StateFlow. Compose provides collectAsStateWithLifecycle to collect flows safely:
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UserUiState(isLoading = true)
val user = repo.getUser(id)
_uiState.value = UserUiState(user = user)
}
}
}
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when {
uiState.isLoading -> CircularProgressIndicator()
uiState.user != null -> UserContent(uiState.user!!)
else -> Text("No user found")
}
}
collectAsStateWithLifecycle automatically stops collection when the composable leaves the composition (e.g., when the app goes to the background), preventing leaks.
ViewModel as State Holder
ViewModels are the recommended state holder for screen-level composables. They survive configuration changes, scope to a navigation destination, and integrate with Hilt for dependency injection.
@HiltViewModel
class DashboardViewModel @Inject constructor(
private val getDashboardData: GetDashboardDataUseCase
) : ViewModel() {
var uiState by mutableStateOf(DashboardUiState())
private set
init { loadData() }
private fun loadData() {
viewModelScope.launch {
uiState = DashboardUiState(isLoading = true)
val data = getDashboardData()
uiState = DashboardUiState(data = data)
}
}
}
Quiz
1. What is the difference between remember and rememberSaveable?
2. What is state hoisting?
3. When should you use derivedStateOf instead of reading state directly?
4. Why is collectAsStateWithLifecycle preferred over collectAsState?
Flashcards
Question
What does remember do in Compose?
Click to reveal answer
Answer
Stores a value in the composition that survives recomposition but is lost on configuration change. Used with mutableStateOf to create observable state.
Question
What is state hoisting and why is it important?
Click to reveal answer
Answer
Moving state from child to parent composable. Makes children stateless, reusable, testable, and enforces single source of truth.
Question
When should you use a ViewModel for state instead of remember?
Click to reveal answer
Answer
When state must survive configuration changes, is needed across navigation events, or holds complex business logic that belongs outside the UI layer.
Question
What problem does derivedStateOf solve?
Click to reveal answer
Answer
It caches derived computations and only triggers recomposition when the result changes, preventing unnecessary recompositions when source state changes frequently.
Revision Notes
Key Takeaways
- 1. remember preserves state across recompositions, rememberSaveable also survives configuration changes
- 2. State hoisting makes composables stateless, reusable, and testable
- 3. derivedStateOf prevents unnecessary recompositions by caching derived values
- 4. ViewModels are the recommended state holder for screen-level composables
- 5. collectAsStateWithLifecycle is preferred over collectAsState for lifecycle safety
Interview Tips
- • Explain state hoisting with a concrete example: before/after code
- • Know when to use remember vs rememberSaveable vs ViewModel
- • Discuss derivedStateOf vs reading state directly for performance
- • Be ready to explain how StateFlow integrates with Compose via collectAsStateWithLifecycle
Cheat Sheet
State Management Cheat Sheet
State Creation:
remember { mutableStateOf(value) }-- survives recompositionrememberSaveable { mutableStateOf(value) }-- survives config changemutableStateListOf()-- observable list
Delegation:
var x by remember { mutableStateOf(0) }-- clean read/write
Hoisting:
- Stateless: state as param, events as lambdas
- Single source of truth in parent
- State holder class for complex state
Derived:
derivedStateOf { ... }-- cache computations- Use when result changes less than source
ViewModel Integration:
collectAsStateWithLifecycle()-- lifecycle-aware flow collection- ViewModel survives config change and navigation