Tools: State Management in Jetpack Compose: remember, mutableStateOf, and Beyond

Tools: State Management in Jetpack Compose: remember, mutableStateOf, and Beyond

Source: Dev.to

State Management in Jetpack Compose: remember, mutableStateOf, and Beyond ## Understanding Composition and Recomposition ## The Basics: remember and mutableStateOf ## remember: Preserving Values Across Recompositions ## mutableStateOf: Creating Observable State ## rememberSaveable: Preserving State Through Configuration Changes ## derivedStateOf: Computing Values from State ## State Hoisting: Lifting State Up ## ViewModel vs Local State: When to Use Each ## Local State (remember) ## ViewModel ## Combining ViewModel with Local State ## Best Practices for State Management ## Conclusion State management is one of the most crucial aspects of building reactive, user-friendly Android applications with Jetpack Compose. Unlike traditional View-based Android development, Compose embraces a declarative approach where the UI is a function of state. This means understanding how to properly manage state is essential for creating efficient, maintainable, and responsive applications. In this comprehensive guide, we'll explore the various state management tools available in Jetpack Compose, from basic primitives like remember and mutableStateOf to advanced patterns like state hoisting and integration with ViewModel. Before diving into state management, it's important to understand how Compose works. When you write a composable function, it's executed as part of Compose's composition process. As state changes, Compose recomposes—it re-executes composable functions to update the UI. The key challenge: values created locally in a composable function are recreated on every recomposition. This is where state management tools come in. The remember function is the foundation of state management in Compose. It allows you to preserve a value across recompositions: In this example, count is preserved across recompositions. When the button is clicked, the state updates, triggering a recomposition that reflects the new value. mutableStateOf creates a state object that Compose observes. When the state changes, any composables that read that state are recomposed: Using the delegation syntax (var ... by) is more concise and idiomatic: While remember preserves state across recompositions, it doesn't survive configuration changes (like screen rotation). For persistence across configuration changes, use rememberSaveable: rememberSaveable uses the Bundle mechanism to save and restore state, similar to traditional Android development. Sometimes you need to compute a value based on state changes, but you don't want recomposition to happen for every change. derivedStateOf creates a derived state that only notifies observers when its value actually changes: In this example, even if searchQuery is updated multiple times rapidly, the expensive search operation only runs when the derived value would actually change. State hoisting is a design pattern where you move state to a common parent composable. This makes state shareable between multiple child composables and easier to test: State hoisting makes your composables more reusable and easier to test because their behavior depends only on parameters, not internal state. Use local state for UI-related state that doesn't need to survive process death: Use ViewModel for business logic and data that should survive process death: The most robust pattern combines both approaches: Hoist state as high as needed: Move state to the lowest common parent of composables that need it. Keep state close to where it's used: Don't hoist state higher than necessary, as it reduces reusability. Use ViewModel for persistent state: Always use ViewModel for data that should survive process death. Avoid mutable shared state: Prefer immutable data structures and unidirectional data flow. Test composables with state hoisting: Hoisted state makes composables easier to test because you can pass in test values. Use rememberSaveable for UI state: If UI state needs to survive configuration changes, use rememberSaveable. State management in Jetpack Compose is straightforward once you understand the core concepts: remember for preserving values, mutableStateOf for observable state, rememberSaveable for configuration change survival, and ViewModel for persistent business logic. The key is choosing the right tool for each situation. Use local state for temporary UI state, ViewModel for persistent business logic, and state hoisting to share state between composables effectively. All 8 templates demonstrate proper state management. https://myougatheax.gumroad.com Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: @Composable fun CounterExample() { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times") } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Composable fun CounterExample() { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times") } } CODE_BLOCK: @Composable fun CounterExample() { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times") } } CODE_BLOCK: @Composable fun LoginForm() { val emailState = remember { mutableStateOf("") } val passwordState = remember { mutableStateOf("") } Column { TextField( value = emailState.value, onValueChange = { emailState.value = it }, label = { Text("Email") } ) TextField( value = passwordState.value, onValueChange = { passwordState.value = it }, label = { Text("Password") } ) } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Composable fun LoginForm() { val emailState = remember { mutableStateOf("") } val passwordState = remember { mutableStateOf("") } Column { TextField( value = emailState.value, onValueChange = { emailState.value = it }, label = { Text("Email") } ) TextField( value = passwordState.value, onValueChange = { passwordState.value = it }, label = { Text("Password") } ) } } CODE_BLOCK: @Composable fun LoginForm() { val emailState = remember { mutableStateOf("") } val passwordState = remember { mutableStateOf("") } Column { TextField( value = emailState.value, onValueChange = { emailState.value = it }, label = { Text("Email") } ) TextField( value = passwordState.value, onValueChange = { passwordState.value = it }, label = { Text("Password") } ) } } CODE_BLOCK: @Composable fun LoginForm() { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } Column { TextField( value = email, onValueChange = { email = it }, label = { Text("Email") } ) TextField( value = password, onValueChange = { password = it }, label = { Text("Password") } ) } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Composable fun LoginForm() { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } Column { TextField( value = email, onValueChange = { email = it }, label = { Text("Email") } ) TextField( value = password, onValueChange = { password = it }, label = { Text("Password") } ) } } CODE_BLOCK: @Composable fun LoginForm() { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } Column { TextField( value = email, onValueChange = { email = it }, label = { Text("Email") } ) TextField( value = password, onValueChange = { password = it }, label = { Text("Password") } ) } } CODE_BLOCK: @Composable fun PersistentCounterExample() { var count by rememberSaveable { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times (survives rotation)") } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Composable fun PersistentCounterExample() { var count by rememberSaveable { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times (survives rotation)") } } CODE_BLOCK: @Composable fun PersistentCounterExample() { var count by rememberSaveable { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times (survives rotation)") } } CODE_BLOCK: @Composable fun TextSearchExample() { var searchQuery by remember { mutableStateOf("") } // This expensive computation only runs when searchQuery actually changes val searchResults by remember(searchQuery) { derivedStateOf { performExpensiveSearch(searchQuery) } } Column { TextField( value = searchQuery, onValueChange = { searchQuery = it }, label = { Text("Search") } ) LazyColumn { items(searchResults.size) { index -> Text(searchResults[index]) } } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Composable fun TextSearchExample() { var searchQuery by remember { mutableStateOf("") } // This expensive computation only runs when searchQuery actually changes val searchResults by remember(searchQuery) { derivedStateOf { performExpensiveSearch(searchQuery) } } Column { TextField( value = searchQuery, onValueChange = { searchQuery = it }, label = { Text("Search") } ) LazyColumn { items(searchResults.size) { index -> Text(searchResults[index]) } } } } CODE_BLOCK: @Composable fun TextSearchExample() { var searchQuery by remember { mutableStateOf("") } // This expensive computation only runs when searchQuery actually changes val searchResults by remember(searchQuery) { derivedStateOf { performExpensiveSearch(searchQuery) } } Column { TextField( value = searchQuery, onValueChange = { searchQuery = it }, label = { Text("Search") } ) LazyColumn { items(searchResults.size) { index -> Text(searchResults[index]) } } } } COMMAND_BLOCK: @Composable fun ParentComponent() { var sharedState by remember { mutableStateOf("") } Column { ChildComponentA( state = sharedState, onStateChange = { sharedState = it } ) ChildComponentB(state = sharedState) } } @Composable fun ChildComponentA( state: String, onStateChange: (String) -> Unit ) { TextField( value = state, onValueChange = onStateChange ) } @Composable fun ChildComponentB(state: String) { Text("Shared state: $state") } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @Composable fun ParentComponent() { var sharedState by remember { mutableStateOf("") } Column { ChildComponentA( state = sharedState, onStateChange = { sharedState = it } ) ChildComponentB(state = sharedState) } } @Composable fun ChildComponentA( state: String, onStateChange: (String) -> Unit ) { TextField( value = state, onValueChange = onStateChange ) } @Composable fun ChildComponentB(state: String) { Text("Shared state: $state") } COMMAND_BLOCK: @Composable fun ParentComponent() { var sharedState by remember { mutableStateOf("") } Column { ChildComponentA( state = sharedState, onStateChange = { sharedState = it } ) ChildComponentB(state = sharedState) } } @Composable fun ChildComponentA( state: String, onStateChange: (String) -> Unit ) { TextField( value = state, onValueChange = onStateChange ) } @Composable fun ChildComponentB(state: String) { Text("Shared state: $state") } CODE_BLOCK: @Composable fun ToggleVisibility() { var isVisible by remember { mutableStateOf(true) } Button(onClick = { isVisible = !isVisible }) { Text(if (isVisible) "Hide" else "Show") } if (isVisible) { Text("Content is visible") } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Composable fun ToggleVisibility() { var isVisible by remember { mutableStateOf(true) } Button(onClick = { isVisible = !isVisible }) { Text(if (isVisible) "Hide" else "Show") } if (isVisible) { Text("Content is visible") } } CODE_BLOCK: @Composable fun ToggleVisibility() { var isVisible by remember { mutableStateOf(true) } Button(onClick = { isVisible = !isVisible }) { Text(if (isVisible) "Hide" else "Show") } if (isVisible) { Text("Content is visible") } } COMMAND_BLOCK: class UserViewModel : ViewModel() { private val _userState = MutableStateFlow<User?>(null) val userState: StateFlow<User?> = _userState.asStateFlow() init { loadUser() } private fun loadUser() { viewModelScope.launch { _userState.value = userRepository.getUser() } } } @Composable fun UserScreen(viewModel: UserViewModel = hiltViewModel()) { val user by viewModel.userState.collectAsState() user?.let { Text("Welcome, ${it.name}") } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: class UserViewModel : ViewModel() { private val _userState = MutableStateFlow<User?>(null) val userState: StateFlow<User?> = _userState.asStateFlow() init { loadUser() } private fun loadUser() { viewModelScope.launch { _userState.value = userRepository.getUser() } } } @Composable fun UserScreen(viewModel: UserViewModel = hiltViewModel()) { val user by viewModel.userState.collectAsState() user?.let { Text("Welcome, ${it.name}") } } COMMAND_BLOCK: class UserViewModel : ViewModel() { private val _userState = MutableStateFlow<User?>(null) val userState: StateFlow<User?> = _userState.asStateFlow() init { loadUser() } private fun loadUser() { viewModelScope.launch { _userState.value = userRepository.getUser() } } } @Composable fun UserScreen(viewModel: UserViewModel = hiltViewModel()) { val user by viewModel.userState.collectAsState() user?.let { Text("Welcome, ${it.name}") } } COMMAND_BLOCK: class ProductViewModel : ViewModel() { private val _products = MutableStateFlow<List<Product>>(emptyList()) val products: StateFlow<List<Product>> = _products.asStateFlow() init { loadProducts() } private fun loadProducts() { viewModelScope.launch { _products.value = productRepository.getProducts() } } } @Composable fun ProductListScreen(viewModel: ProductViewModel = hiltViewModel()) { val products by viewModel.products.collectAsState() var selectedProductId by remember { mutableStateOf<String?>(null) } Row { LazyColumn(modifier = Modifier.weight(1f)) { items(products) { product -> ProductItem( product = product, isSelected = selectedProductId == product.id, onSelect = { selectedProductId = product.id } ) } } selectedProductId?.let { id -> ProductDetails( product = products.find { it.id == id }, modifier = Modifier.weight(1f) ) } } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: class ProductViewModel : ViewModel() { private val _products = MutableStateFlow<List<Product>>(emptyList()) val products: StateFlow<List<Product>> = _products.asStateFlow() init { loadProducts() } private fun loadProducts() { viewModelScope.launch { _products.value = productRepository.getProducts() } } } @Composable fun ProductListScreen(viewModel: ProductViewModel = hiltViewModel()) { val products by viewModel.products.collectAsState() var selectedProductId by remember { mutableStateOf<String?>(null) } Row { LazyColumn(modifier = Modifier.weight(1f)) { items(products) { product -> ProductItem( product = product, isSelected = selectedProductId == product.id, onSelect = { selectedProductId = product.id } ) } } selectedProductId?.let { id -> ProductDetails( product = products.find { it.id == id }, modifier = Modifier.weight(1f) ) } } } COMMAND_BLOCK: class ProductViewModel : ViewModel() { private val _products = MutableStateFlow<List<Product>>(emptyList()) val products: StateFlow<List<Product>> = _products.asStateFlow() init { loadProducts() } private fun loadProducts() { viewModelScope.launch { _products.value = productRepository.getProducts() } } } @Composable fun ProductListScreen(viewModel: ProductViewModel = hiltViewModel()) { val products by viewModel.products.collectAsState() var selectedProductId by remember { mutableStateOf<String?>(null) } Row { LazyColumn(modifier = Modifier.weight(1f)) { items(products) { product -> ProductItem( product = product, isSelected = selectedProductId == product.id, onSelect = { selectedProductId = product.id } ) } } selectedProductId?.let { id -> ProductDetails( product = products.find { it.id == id }, modifier = Modifier.weight(1f) ) } } } - Toggle visibility of UI elements - Text field input during editing - Scroll position - Temporary UI state - User data from a database or API - Application state - Business logic - State that persists across the app lifecycle - Hoist state as high as needed: Move state to the lowest common parent of composables that need it. - Keep state close to where it's used: Don't hoist state higher than necessary, as it reduces reusability. - Use ViewModel for persistent state: Always use ViewModel for data that should survive process death. - Avoid mutable shared state: Prefer immutable data structures and unidirectional data flow. - Test composables with state hoisting: Hoisted state makes composables easier to test because you can pass in test values. - Use rememberSaveable for UI state: If UI state needs to survive configuration changes, use rememberSaveable.