How to manage state in Jetpack Compose

Using remember, mutableStateOf, StateFlow, ViewModel, and state hoisting patterns in Compose

, updated

State management is central to Jetpack Compose. This guide covers the tools and patterns from local state to ViewModel integration.

remember and mutableStateOf — local state

Best for: UI-only state that doesn’t need to survive configuration changes.

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

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}
  • remember keeps the value across recompositions
  • mutableStateOf creates an observable state holder
  • Use by delegate for cleaner syntax (requires import androidx.compose.runtime.getValue and setValue)

Other state holders

// MutableStateList — observable list
val items = remember { mutableStateListOf("Apple", "Banana") }
items.add("Cherry")

// MutableStateMap — observable map
val scores = remember { mutableStateMapOf("Alice" to 95) }
scores["Bob"] = 87

// rememberSaveable — survives configuration changes
var text by rememberSaveable { mutableStateOf("") }

StateFlow and ViewModel — app state

Best for: business logic, state that survives configuration changes, and state shared across screens.

class CounterViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() {
        _count.value++
    }
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val count by viewModel.count.collectAsStateWithLifecycle()

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Count: $count")
        Button(onClick = { viewModel.increment() }) {
            Text("Increment")
        }
    }
}

Use collectAsStateWithLifecycle() (from androidx.lifecycle:lifecycle-runtime-compose) instead of collectAsState() to respect the lifecycle — it stops collecting when the app goes to the background.

State hoisting

Move state to the caller so the composable becomes stateless and reusable:

// Before — stateful (hard to test, hard to reuse)
@Composable
fun SearchBar() {
    var query by remember { mutableStateOf("") }
    TextField(
        value = query,
        onValueChange = { query = it },
        label = { Text("Search") }
    )
}

// After — stateless (hoisted)
@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    TextField(
        value = query,
        onValueChange = onQueryChange,
        label = { Text("Search") },
        modifier = modifier
    )
}

// The caller manages the state
@Composable
fun SearchScreen() {
    var query by rememberSaveable { mutableStateOf("") }
    SearchBar(query = query, onQueryChange = { query = it })
}

Side effects

For operations that need to happen outside of composition:

// LaunchedEffect — run a suspend function when a key changes
@Composable
fun UserProfile(userId: String, viewModel: UserViewModel) {
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }

    val user by viewModel.user.collectAsStateWithLifecycle()
    if (user != null) {
        Text(user!!.name)
    }
}

// DisposableEffect — register/unregister callbacks
@Composable
fun NetworkMonitor(networkCallback: NetworkCallback) {
    DisposableEffect(Unit) {
        val manager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        manager.registerNetworkCallback(networkRequest, networkCallback)
        onDispose {
            manager.unregisterNetworkCallback(networkCallback)
        }
    }
}

// rememberCoroutineScope — launch coroutines from event handlers
@Composable
fun SaveButton(viewModel: SaveViewModel) {
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch { viewModel.save() }
    }) {
        Text("Save")
    }
}

// SideEffect — publish state to non-Compose code after every recomposition
@Composable
fun AnalyticsTracker(screenName: String) {
    SideEffect {
        analytics.trackScreenView(screenName)
    }
}

Which state approach to use?

Pattern Best for Survives config change
remember + mutableStateOf Local UI state, simple toggles No
rememberSaveable Local state that survives rotation Yes
MutableStateList/Map Observable collections No
ViewModel + StateFlow Business logic, shared state Yes
State hoisting Reusable, testable components Depends on caller