S
Subhankar Bag
Guest
![[TrendyMediaToday.com] The Hidden Dangers of Jetpack Compose State (And How to Fix Them With Real Examples) {file_size} {filename} [TrendyMediaToday.com] The Hidden Dangers of Jetpack Compose State (And How to Fix Them With Real Examples) {file_size} {filename}](https://cdn-images-1.medium.com/max/1024/1*eGFrDoLlsFjY2a--cV_iHQ.png)
You add a simple counter to your Compose screen.
It should take five minutes.
Three hours later, your UI isnβt updating, recompositions are exploding, and your remember variables are mysteriously resetting.
Sound familiar?
This is what happens when Jetpack Compose state meets lifecycles, recomposition, and developer assumptions.
The good news? State in Compose is powerfulβββbut only if you know its traps.
In this article, Iβll walk you through state management in real-world scenarios:
- Persistent forms that survive rotation and navigation
- Lists that donβt flicker or reset scroll position
- Navigation flows with multiple screens sharing state
- Best practices to prevent unnecessary recompositions
1. The Innocent Beginning: βJust Use rememberβ
Every Compose beginner starts here:
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
Works fine⦠until:
- Rotate β counter resets to 0
- Navigate away β counter resets again
For quick demos, no big deal. In production? Your users wonβt tolerate disappearing state.
2. Real-World Scenario 1: Forms That Donβt Lose Data
Problem: Form data resets on rotation
Imagine a sign-up form:
@Composable
fun SignUpForm() {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column {
TextField(value = email, onValueChange = { email = it })
TextField(value = password, onValueChange = { password = it })
Button(onClick = { /* submit */ }) {
Text("Sign Up")
}
}
}
Rotate the device β everything clears out. Users rage-quit.
Fix: Use rememberSaveable for form fields
@Composable
fun SignUpForm() {
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
Column {
TextField(value = email, onValueChange = { email = it })
TextField(value = password, onValueChange = { password = it })
Button(onClick = { /* submit */ }) {
Text("Sign Up")
}
}
}
Now values survive rotation because Compose saves them in a Bundle.
Pro Tip: For complex objects, provide a custom Saver.
data class Address(val street: String, val city: String)
val AddressSaver = Saver<Address, String>(
save = { "${it.street}|${it.city}" },
restore = { val (street, city) = it.split("|"); Address(street, city) }
)
var address by rememberSaveable(stateSaver = AddressSaver) {
mutableStateOf(Address("", ""))
}
3. Real-World Scenario 2: Lists Without Flicker or Reset
Problem: Scroll resets when data changes
Typical list implementation:
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users) { user ->
Text(user.name)
}
}
}
If the list updates β scroll position resets. Worse, if your list item state (like checkboxes) lives inside the item Composable, it resets too.
Fix 1: Provide stable keys
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(
items = users,
key = { it.id } // Stable key prevents item reset
) { user ->
Text(user.name)
}
}
}
Fix 2: Hoist item state
Bad practice: keeping state inside the item.
@Composable
fun UserItem(user: User) {
var checked by remember { mutableStateOf(false) }
Checkbox(checked = checked, onCheckedChange = { checked = it })
}
On scroll, items get recomposed β checkbox resets.
Instead, hoist state to the parent list.
@Composable
fun UserList(users: List<User>) {
var checkedIds by rememberSaveable { mutableStateOf(setOf<Int>()) }
LazyColumn {
items(users, key = { it.id }) { user ->
val checked = checkedIds.contains(user.id)
Row {
Text(user.name)
Checkbox(
checked = checked,
onCheckedChange = { isChecked ->
checkedIds = if (isChecked) {
checkedIds + user.id
} else {
checkedIds - user.id
}
}
)
}
}
}
}
Now scroll is preserved, and item state survives recomposition.
4. Real-World Scenario 3: Navigation With Shared State
Problem: State resets across screens
Example with Navigation-Compose:
@Composable
fun AppNav() {
val navController = rememberNavController()
NavHost(navController, startDestination = "form") {
composable("form") { SignUpForm() }
composable("summary") { SummaryScreen() }
}
}
If you navigate from form β summary, the state from SignUpForm is gone.
Fix: Use a shared ViewModel
Scope a ViewModel to the NavGraph, not the Composable.
class SignUpViewModel : ViewModel() {
var email by mutableStateOf("")
var password by mutableStateOf("")
}
@Composable
fun SignUpForm(viewModel: SignUpViewModel = viewModel()) {
Column {
TextField(value = viewModel.email, onValueChange = { viewModel.email = it })
TextField(value = viewModel.password, onValueChange = { viewModel.password = it })
Button(onClick = { /* submit */ }) {
Text("Sign Up")
}
}
}
@Composable
fun SummaryScreen(viewModel: SignUpViewModel = viewModel()) {
Column {
Text("Email: ${viewModel.email}")
Text("Password: ${viewModel.password}")
}
}
Now the same ViewModel instance holds state across navigation.
5. Bonus: Preventing Unnecessary Recompositions
Problem: Every update re-renders the entire tree
@Composable
fun ShoppingCart(cartItems: List<CartItem>) {
Column {
Text("Total: ${cartItems.sumOf { it.price }}")
LazyColumn {
items(cartItems) { item ->
CartItemRow(item)
}
}
}
}
Every time one item changes β total recalculates β whole UI redraws.
Fix: Use derivedStateOf and stability
@Composable
fun ShoppingCart(cartItems: List<CartItem>) {
val total by remember(cartItems) {
derivedStateOf { cartItems.sumOf { it.price } }
}
Column {
Text("Total: $total")
LazyColumn {
items(cartItems, key = { it.id }) { item ->
CartItemRow(item)
}
}
}
}
Also mark your data class stable for efficiency:
@Immutable
data class CartItem(val id: Int, val name: String, val price: Int)
6. Your Compose State Playbook
Hereβs the cheat sheet I wish I had when I started:
- remember β Ephemeral UI state (expanded, focused, selected).
- rememberSaveable β UI state that should survive rotation (text input, tab selection).
- ViewModel + Flow/LiveData β Business logic, screen state, repository data.
- Immutable data + derivedStateOf β Prevent wasted recompositions.
- Stable keys in LazyColumn β Prevent flicker & state reset.
- Hoist item state β Keep lists predictable.
- Shared ViewModel across NavGraph β Persist state across screens.
7. The Payoff
Since our team adopted this layered state approach:
- Rotation-related bug reports dropped by 90%
- No more βlist flickersβ from unnecessary recompositions
- Navigation feels smooth, forms survive back-and-forth screens
- New hires onboard faster with a clear mental model
Jetpack Compose state isnβt just βthrow remember everywhere.β
Itβs about choosing the right persistence level: ephemeral β saveable β ViewModel.
Get this right, and your Compose apps feel bulletproof.
Get it wrong, and youβre stuck debugging nightmares at 2 AM.


The Hidden Dangers of Jetpack Compose State (And How to Fix Them With Real Examples) was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.
Continue reading...