DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Compose State Management: Complete Guide from remember to UiState

Compose State Management: Complete Guide from remember to UiState

Jetpack Compose state management can feel overwhelming. Should you use remember, ViewModel, StateFlow? Let me break down every approach with clear use cases.

1. remember & mutableStateOf: Local UI State

Use this for simple, temporary UI state that doesn't survive configuration changes:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

When: Toggle visibility, form inputs, temp animation values
Avoid: Data that should survive rotation or app kill

2. rememberSaveable: Surviving Config Changes

Automatically saves state to Bundle during configuration changes:

@Composable
fun FormScreen() {
    var name by rememberSaveable { mutableStateOf("") }
    var email by rememberSaveable { mutableStateOf("") }

    // State persists across rotation
    TextField(value = name, onValueChange = { name = it })
}
Enter fullscreen mode Exit fullscreen mode

When: Form inputs, user preferences, temporary selections

3. State Hoisting: Composable Reusability

Move state up to make composables reusable:

@Composable
fun Button(value: String, onValueChange: (String) -> Unit) {
    // Stateless button
}

@Composable
fun Container() {
    var state by remember { mutableStateOf("") }
    Button(value = state, onValueChange = { state = it })
}
Enter fullscreen mode Exit fullscreen mode

When: Building component libraries, testing without mocks

4. ViewModel + StateFlow: Persistent Screen State

For business logic and data that survives app kill:

class UserViewModel : ViewModel() {
    private val _userData = MutableStateFlow<User?>(null)
    val userData: StateFlow<User?> = _userData.asStateFlow()

    init {
        viewModelScope.launch {
            _userData.value = userRepository.getUser()
        }
    }
}

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val user by viewModel.userData.collectAsStateWithLifecycle()
    Text("${user?.name}")
}
Enter fullscreen mode Exit fullscreen mode

When: API calls, database queries, persistent user data

5. UiState Sealed Class: Handling Loading/Error/Success

sealed class UserUiState {
    object Loading : UserUiState()
    data class Success(val user: User) : UserUiState()
    data class Error(val exception: Exception) : UserUiState()
}

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun loadUser() {
        viewModelScope.launch {
            try {
                val user = repository.getUser()
                _uiState.value = UserUiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UserUiState.Error(e)
            }
        }
    }
}

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when (uiState) {
        is UserUiState.Loading -> CircularProgressIndicator()
        is UserUiState.Success -> UserCard((uiState as UserUiState.Success).user)
        is UserUiState.Error -> ErrorMessage((uiState as UserUiState.Error).exception)
    }
}
Enter fullscreen mode Exit fullscreen mode

When: Any screen with async operations

6. derivedStateOf: Computed Values

Avoid recompositions when intermediate values change:

@Composable
fun List(items: List<String>) {
    val filteredItems by remember(items) {
        derivedStateOf { items.filter { it.length > 3 } }
    }
    // Recomposes only when filtered list actually changes
}
Enter fullscreen mode Exit fullscreen mode

When: Complex filters, expensive calculations on state

7. collectAsStateWithLifecycle: The Safe Collector

Automatically handles lifecycle to prevent memory leaks:

@Composable
fun Screen(viewModel: MyViewModel = viewModel()) {
    val state by viewModel.stateFlow.collectAsStateWithLifecycle()
    // Safely observes without manual lifecycle management
}
Enter fullscreen mode Exit fullscreen mode

When: Always use this instead of collect in Compose

Comparison Table

Approach Scope Survives Rotation Survives Kill Async Support Best For
remember Composition UI toggles, animations
rememberSaveable Composition Form inputs
ViewModel+StateFlow Screen Business logic & data
UiState pattern Screen Async operations

The Golden Rule

  • Simple UI stateremember
  • Persist across rotationrememberSaveable
  • API calls, databaseViewModel + StateFlow
  • Loading/Error/SuccessUiState sealed class

Master this hierarchy and your Compose architecture will be solid.


Ready to build production Android apps?

8 Android App Templates → https://myougatheax.gumroad.com

Top comments (0)