Popular Searches
Popular Course Categories
Popular Courses

Managing UI State with remember and mutableStateOf

Managing UI State with remember and mutableStateOf

Modern Android UI with Jetpack Compose

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

  1. A composable reads a state value.
  2. Compose records that the composable depends on the state.
  3. A user action or another operation changes the state value.
  4. Compose identifies the affected composables.
  5. Recomposition runs the relevant composable functions again.
  6. 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

  1. The name state starts as an empty string.
  2. The text field displays the current value of name.
  3. When the user types, onValueChange updates the state.
  4. The submit button copies the trimmed value into submittedName.
  5. 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

  1. What problem does UI state solve in Jetpack Compose?
  2. Explain the difference between remember and mutableStateOf.
  3. Write a composable that toggles a message using Boolean state.
  4. Create a counter with increment, decrement, and reset buttons.
  5. Why does modifying a regular Kotlin variable not automatically update the UI?
  6. When would you choose rememberSaveable instead of remember?
  7. Explain state hoisting with a reusable button component.
  8. How would you manage a dynamic list of tasks in Compose?
  9. What role does a ViewModel play in screen-level state management?
  10. 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.

Previous Next
whatsapp