Managing UI State with remember and mutableStateOf in Android Jetpack Compose
Course: Android App Development
Topic: Managing UI State with remember and mutableStateOf
Course Link: Android App Development Course
Demo Link: Register for a Free Course Demo
Website: JustAcademy
1. Introduction to UI State Management
UI state management is an essential concept in Android application development. It determines how an application's user interface responds to user interactions, data changes, and application events. In Jetpack Compose, UI state helps developers build dynamic, interactive, and responsive interfaces using a declarative programming approach.
For example, consider a counter application. When a user clicks an increment button, the displayed number should increase immediately. Similarly, when a user enters text into an input field, the interface should reflect the updated value. These behaviors are controlled through state.
Jetpack Compose provides several state-management tools, including remember, mutableStateOf, rememberSaveable, and state holders such as ViewModels. Understanding remember and mutableStateOf is an important first step toward building interactive Android applications.
Learning Objectives
- Understand UI state and its importance in Android applications.
- Learn the purpose of
remember and mutableStateOf.
- Understand how recomposition works in Jetpack Compose.
- Create interactive buttons, counters, and text fields.
- Manage Boolean, integer, string, and other state values.
- Understand the difference between ordinary variables and observable state.
- Learn state hoisting, common mistakes, and best practices.
2. What Is UI State?
UI state represents the data that determines what the user sees on the screen at a particular moment. When that data changes, the interface may need to update to display the new information.
Common examples of UI state include:
- The current text entered into a search box.
- Whether a checkbox is selected.
- The number displayed by a counter.
- Whether a password is visible or hidden.
- Whether a dialog or menu is open.
- The currently selected tab or navigation item.
- Whether a loading indicator should be displayed.
Example of UI State
var count = 0
The variable above stores a counter value. However, changing this ordinary variable does not automatically notify Jetpack Compose that the displayed interface needs to be updated. For reactive UI updates, the value should be represented using observable state.
3. Understanding mutableStateOf
mutableStateOf() creates an observable state holder containing a value. When the value changes, Compose can detect the change and recompose the composable functions that read that state.
It is commonly used with integers, strings, Boolean values, and other supported data types.
Syntax
val state = mutableStateOf(initialValue)
Here, initialValue is the starting value of the state. The returned state object exposes a mutable value property.
Example: Creating Mutable State
import androidx.compose.runtime.mutableStateOf
val count = mutableStateOf(0)
// Read the current value
val currentCount = count.value
// Update the value
count.value = 5
In this example, the state starts at zero. Reading count.value retrieves its current value, while assigning a new value updates the state holder. When Compose observes the state in a composable, changes can trigger recomposition.
Important Characteristics of mutableStateOf
- It stores a value inside a state object.
- Its
value property can be updated.
- Compose observes reads of the state during composition.
- Changing the value can trigger recomposition of affected composables.
- It does not, by itself, preserve state across activity recreation or process death.
4. Understanding remember
remember is a Jetpack Compose function that stores an object across recompositions of the same composition location. It is useful when a composable needs to retain a value while Compose redraws part of the interface.
Without remember, a value created directly inside a composable may be recreated whenever that composable executes again.
Syntax
val value = remember {
mutableStateOf(initialValue)
}
When Compose recomposes the composable, the remembered state object is reused rather than recreated, as long as it remains at the same composition location and its relevant keys have not changed.
Example: Using remember
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.material3.Text
@Composable
fun Greeting() {
val name = remember {
mutableStateOf("Student")
}
Text(text = "Hello, ${name.value}!")
}
In this example, the name is initialized as "Student". The remember function retains the state object through recompositions, allowing updated values to remain available while the composable stays in the composition.
5. Combining remember and mutableStateOf
The combination of remember and mutableStateOf is a common pattern for local UI state in Jetpack Compose.
mutableStateOf creates an observable, mutable state holder.
remember retains that state holder across recompositions.
Recommended Syntax
var count by remember {
mutableStateOf(0)
}
This version uses Kotlin property delegation. It requires the getValue and setValue extension imports, which are usually added automatically by Android Studio.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
With delegation, you can read and update the variable directly without writing .value each time.
Equivalent Versions
Using the value property:
val count = remember {
mutableStateOf(0)
}
count.value = count.value + 1
Using property delegation:
var count by remember {
mutableStateOf(0)
}
count++
Both approaches can manage observable state. Property delegation often makes composable code shorter and easier to read.
6. How Recomposition Works
Recomposition is the process through which Jetpack Compose reruns composable functions when observable state they read changes. Compose then updates the affected parts of the interface as needed.
Recomposition Process
- A composable reads a state value.
- Compose records that the composable depends on the state.
- A user action or another operation changes the state value.
- Compose identifies the affected composables.
- Recomposition runs the relevant composable functions again.
- The interface reflects the updated state.
Example
@Composable
fun CounterExample() {
var count by remember {
mutableStateOf(0)
}
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
When the button is clicked, count increases by one. Since the displayed text reads the state, Compose updates the text during recomposition.
Important: Recomposition does not mean that the entire screen must be recreated. Compose can intelligently update the affected parts of the UI.
7. Complete Example: Building a Counter Application
This example demonstrates how to create a counter using remember and mutableStateOf. It includes increment, decrement, and reset operations.
Complete Kotlin Code
package com.example.uistate
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun CounterScreen() {
var count by remember {
mutableStateOf(0)
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Counter Application",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Current Count: $count",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
Button(
onClick = {
if (count > 0) {
count--
}
}
) {
Text("Decrement")
}
Button(onClick = { count = 0 }) {
Text("Reset")
}
}
}
Code Explanation
| Code Element |
Purpose |
@Composable |
Marks a function that describes UI in Jetpack Compose. |
remember |
Retains the state object across recompositions. |
mutableStateOf(0) |
Creates an observable integer state initialized to zero. |
var count by |
Provides convenient access to the state value using property delegation. |
count++ |
Increases the count and updates dependent UI. |
count-- |
Decreases the count when the value is greater than zero. |
count = 0 |
Resets the counter to its initial value. |
Text |
Displays the current state value. |
Button |
Provides an interactive control that changes state. |
8. Managing Different Types of UI State
Jetpack Compose can manage different state types depending on the needs of the interface.
8.1 Integer State
Integer state is useful for counters, quantities, item counts, and numeric selections.
var quantity by remember {
mutableStateOf(1)
}
Button(onClick = { quantity++ }) {
Text("Add Item")
}
Text("Quantity: $quantity")
8.2 Boolean State
Boolean state represents two possible conditions, such as visible or hidden, selected or unselected, and open or closed.
var isVisible by remember {
mutableStateOf(false)
}
Button(onClick = {
isVisible = !isVisible
}) {
Text("Toggle Message")
}
if (isVisible) {
Text("Welcome to Jetpack Compose!")
}
8.3 String State
String state is useful for form inputs, search fields, usernames, and messages.
var username by remember {
mutableStateOf("")
}
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Enter Username") }
)
Text("Hello, $username")
For this example, import OutlinedTextField from androidx.compose.material3.
8.4 List State
For a mutable list displayed in the UI, use an observable collection such as mutableStateListOf, or replace a list stored in state with a new list when updating it.
import androidx.compose.runtime.mutableStateListOf
val tasks = remember {
mutableStateListOf("Learn Kotlin", "Learn Compose")
}
Button(onClick = {
tasks.add("Build an Android App")
}) {
Text("Add Task")
}
tasks.forEach { task ->
Text(text = task)
}
A regular mutable list does not automatically notify Compose when its contents change. Use an observable collection or an appropriate immutable-state update pattern instead.
9. Managing Text Input with State
Text input is one of the most common examples of state management. A text field should display the current value and report changes back to the state holder. This creates a unidirectional data flow between the state and the UI.
Example: Name Input Form
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@Composable
fun NameInputScreen() {
var name by remember {
mutableStateOf("")
}
var submittedName by remember {
mutableStateOf("")
}
Column {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Your Name") }
)
Button(
onClick = {
submittedName = name.trim()
}
) {
Text("Submit")
}
if (submittedName.isNotEmpty()) {
Text("Welcome, $submittedName!")
}
}
}
How It Works
- The name state starts as an empty string.
- The text field displays the current value of
name.
- When the user types,
onValueChange updates the state.
- The submit button copies the trimmed value into
submittedName.
- The welcome message appears when a name has been submitted.
In real applications, validate input before processing it and avoid storing passwords or other sensitive data in UI state longer than necessary.
10. Difference Between remember and mutableStateOf
Although these functions are frequently used together, they perform different jobs.
| Feature |
remember |
mutableStateOf |
| Main purpose |
Retains an object across recompositions. |
Creates an observable state holder. |
| Stores state by itself |
Stores the result of the supplied calculation. |
Holds a mutable value in its state object. |
| Observability |
Does not automatically make every stored object observable. |
Provides observable reads and writes for its value. |
| Triggers recomposition |
Not by itself; depends on what changes. |
Changing an observed value can trigger recomposition. |
| Typical usage |
remember { ... } |
mutableStateOf(0) |
| Common combination |
remember { mutableStateOf(0) } |
Used to create the observable value retained by remember. |
11. Difference Between Ordinary Variables and Compose State
| Ordinary Variable |
Compose State |
| Stores a value in the usual Kotlin manner. |
Stores a value in a Compose-observable state holder. |
| Changes do not automatically notify Compose. |
Observed value changes can notify Compose. |
| May be reinitialized if declared inside a recomposing function. |
When wrapped with remember, the state object is retained across recompositions. |
| Useful for temporary calculations and local logic. |
Useful for values that control the UI. |
Incorrect Approach
@Composable
fun IncorrectCounter() {
var count = 0
Button(onClick = {
count++
}) {
Text("Count: $count")
}
}
This code does not use observable state. Clicking the button changes the local variable, but Compose is not notified to update the displayed text. A later recomposition may also reset the variable to zero.
Correct Approach
@Composable
fun CorrectCounter() {
var count by remember {
mutableStateOf(0)
}
Button(onClick = {
count++
}) {
Text("Count: $count")
}
}
This version retains the state across recompositions and allows Compose to react to changes.
12. Understanding rememberSaveable
remember retains state across recompositions but does not automatically save it across activity recreation, such as many configuration changes. When a composable leaves the composition, its remembered state is generally discarded.
rememberSaveable can preserve supported state across activity recreation and restore it after process recreation when Android's saved-instance-state mechanism is available.
Example
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun SavedCounter() {
var count by rememberSaveable {
mutableStateOf(0)
}
Button(onClick = {
count++
}) {
Text("Count: $count")
}
}
| Feature |
remember |
rememberSaveable |
| Survives recomposition |
Yes |
Yes |
| Survives activity recreation |
Not by itself |
Usually, for supported saveable values |
| Restores saved values after process recreation |
No |
Can restore from saved instance state |
| Suitable for |
Temporary UI state |
Small UI values that should survive recreation |
For larger or business-critical application state, use an appropriate state holder such as a ViewModel and persistent storage when required. Saved UI state is not a substitute for a database.
13. State Hoisting in Jetpack Compose
State hoisting is the practice of moving state from a child composable to its caller so that the caller controls the state and passes the current value and update callbacks to the child.
It helps make components reusable, easier to test, and easier to coordinate.
Example: Hoisted Counter
@Composable
fun CounterControls(
count: Int,
onIncrement: () -> Unit,
onReset: () -> Unit
) {
Column {
Text("Count: $count")
Button(onClick = onIncrement) {
Text("Increment")
}
Button(onClick = onReset) {
Text("Reset")
}
}
}
@Composable
fun CounterContainer() {
var count by remember {
mutableStateOf(0)
}
CounterControls(
count = count,
onIncrement = { count++ },
onReset = { count = 0 }
)
}
Benefits of State Hoisting
- Creates reusable composable components.
- Separates UI presentation from state ownership.
- Makes components easier to test.
- Allows multiple components to share a single state source.
- Improves code organization and maintainability.
14. Using remember with Keys
The remember function can accept keys. When a key changes, Compose recalculates the remembered value at that composition location.
Example
@Composable
fun UserGreeting(userId: String) {
val greeting = remember(userId) {
"Welcome, user $userId"
}
Text(text = greeting)
}
When userId changes, the remembered greeting is recalculated. Without the key, the value could remain associated with the earlier user ID while the composable stays at the same composition location.
Remember keys are useful for values derived from inputs, but they are not a replacement for observing data sources or managing asynchronous work correctly.
15. Common Mistakes in UI State Management
Mistake 1: Using an Ordinary Variable for UI Updates
An ordinary variable does not automatically trigger recomposition. Use Compose state for values that directly control the UI.
Mistake 2: Forgetting remember
Using mutableStateOf directly inside a composable can create a new state holder on each recomposition. This may reset the value. Use remember or another suitable state holder.
Mistake 3: Expecting remember to Survive Every Lifecycle Event
remember does not provide persistent storage. Use rememberSaveable, ViewModel state, or a database according to the required lifetime of the data.
Mistake 4: Mutating a Regular List
Changing a normal mutable list does not automatically notify Compose. Use mutableStateListOf or replace an observable state value with an updated collection.
Mistake 5: Keeping Too Much State in One Composable
Large composables that manage unrelated state can become difficult to understand. Break the interface into smaller composables and hoist shared state when appropriate.
Mistake 6: Updating State Unnecessarily
Avoid repeatedly assigning values when no meaningful change is needed. Use clear state-update logic and let Compose handle recomposition.
Mistake 7: Performing Side Effects During Composition
Do not perform network requests, database writes, or other uncontrolled side effects directly in the composable body. Use suitable effect APIs, ViewModels, and lifecycle-aware data collection.
16. Best Practices for Managing UI State
- Use
remember { mutableStateOf(...) } for simple local state that should survive recompositions.
- Use property delegation with
by for concise, readable state access.
- Use
rememberSaveable when a small UI value needs to survive supported recreation events.
- Hoist state when multiple composables need to read or update the same value.
- Keep composables focused on rendering UI rather than handling all business logic.
- Use ViewModels for screen-level state and business logic that should survive configuration changes.
- Use observable collections or immutable update patterns for lists and other collections.
- Keep state as close as practical to the UI that needs it, unless sharing or lifecycle requirements suggest otherwise.
- Use clear variable names such as
isLoading, selectedTab, and username.
- Validate user input before submitting or saving it.
- Test initial state, state changes, and relevant lifecycle behavior.
17. Practical Exercise: Build a Like Button
Create a small screen with a Like button. When the user clicks the button, the displayed like count should increase. Use remember and mutableStateOf.
Starter Code
@Composable
fun LikeButtonExample() {
var likes by remember {
mutableStateOf(0)
}
Column {
Text("Likes: $likes")
Button(onClick = {
// TODO: Increase likes
}) {
Text("Like")
}
}
}
Expected Behavior
- The initial like count should be zero.
- Clicking Like should increase the count by one.
- The displayed count should update automatically.
- The state should remain intact during ordinary recompositions.
Solution
@Composable
fun LikeButtonExample() {
var likes by remember {
mutableStateOf(0)
}
Column {
Text("Likes: $likes")
Button(onClick = {
likes++
}) {
Text("Like")
}
}
}
18. Mini Project: Simple Login Form
Build a login form using Compose state. The screen should include an email field, a password field, and a login button. Display a message when the user submits non-empty values.
Example Implementation
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@Composable
fun SimpleLoginForm() {
var email by remember {
mutableStateOf("")
}
var password by remember {
mutableStateOf("")
}
var message by remember {
mutableStateOf("")
}
Column {
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") }
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") }
)
Button(onClick = {
message = if (
email.isNotBlank() && password.isNotBlank()
) {
"Form submitted"
} else {
"Please complete all fields"
}
}) {
Text("Login")
}
Text(message)
}
}
Note: This is a UI-state learning example, not a secure authentication implementation. A real application should use a proper authentication service, secure transport, and appropriate credential handling. Avoid logging or persisting passwords unnecessarily.
19. Interview Questions and Answers
Q1. What is UI state in Jetpack Compose?
Answer: UI state is data that determines what is displayed in the interface. Examples include input text, selected options, loading status, and counter values.
Q2. What is mutableStateOf?
Answer: mutableStateOf creates an observable state holder whose value can be changed. Compose can react when a composable reads that state and its value changes.
Q3. What is the purpose of remember?
Answer: remember retains a calculated object across recompositions at the same composition location, provided its keys remain unchanged.
Q4. Why are remember and mutableStateOf often used together?
Answer: mutableStateOf makes a value observable, while remember retains the state holder across recompositions.
Q5. What is recomposition?
Answer: Recomposition is the process of rerunning affected composable functions when observed state or other relevant inputs change.
Q6. What is the difference between remember and rememberSaveable?
Answer: remember retains values across recompositions, while rememberSaveable can also save and restore supported values across activity recreation and saved-instance-state restoration.
Q7. Does changing an ordinary Kotlin variable trigger recomposition?
Answer: No. An ordinary variable is not automatically observable by Compose. Use an appropriate observable state mechanism for values that control the UI.
Q8. What is state hoisting?
Answer: State hoisting moves state ownership to a caller and passes the current value and update callbacks to child composables.
Q9. Can mutableStateOf store a String or Boolean?
Answer: Yes. It can hold different supported types, including integers, strings, Boolean values, and other objects.
Q10. When should a ViewModel be used?
Answer: A ViewModel is useful for screen-level state and business logic that should survive configuration changes and be separated from UI rendering.
Q11. Why might a counter reset after recomposition?
Answer: The counter may have been declared as an ordinary local variable or the state holder may have been recreated without being remembered.
Q12. How can mutable list updates be observed by Compose?
Answer: Use an observable collection such as mutableStateListOf, or update a state-held immutable list by assigning a new list value.
20. Quick Revision Notes
| Concept |
Quick Revision |
| UI State |
Data that determines the current appearance and behavior of the UI. |
| mutableStateOf |
Creates an observable state holder. |
| remember |
Retains an object across recompositions. |
| Recomposition |
Updates affected UI when observed state changes. |
| rememberSaveable |
Preserves supported UI state across saved-instance-state restoration. |
| State Hoisting |
Moves state ownership to a caller and passes values and callbacks to children. |
| ViewModel |
Manages screen-level state and logic outside the composable UI. |
| Observable Collections |
Help Compose respond to collection changes. |
21. Practice Questions
- What problem does UI state solve in Jetpack Compose?
- Explain the difference between
remember and mutableStateOf.
- Write a composable that toggles a message using Boolean state.
- Create a counter with increment, decrement, and reset buttons.
- Why does modifying a regular Kotlin variable not automatically update the UI?
- When would you choose
rememberSaveable instead of remember?
- Explain state hoisting with a reusable button component.
- How would you manage a dynamic list of tasks in Compose?
- What role does a ViewModel play in screen-level state management?
- How can you prevent invalid input from being submitted in a form?
22. Conclusion
Managing UI state with remember and mutableStateOf is a foundational skill for Android developers working with Jetpack Compose. The combination allows composables to retain observable values across recompositions and respond to user interactions without manually updating the interface.
By practicing with counters, text fields, toggle buttons, and forms, developers can learn how to build responsive interfaces using Compose's declarative model. As applications grow, concepts such as state hoisting, rememberSaveable, ViewModels, and persistent storage help keep state management maintainable and reliable.
Additional Learning Resources
Explore Android App Development training and practical learning opportunities through JustAcademy.