State Management in Jetpack Compose: remember, MutableState, ViewModel, StateFlow, LiveData, and Effect APIs
Jetpack Compose’s state management techniques—including remember, mutableStateOf, ViewModel integration, StateFlow, LiveData, state lifting, and various side‑effect APIs such as LaunchedEffect, rememberCoroutineScope, and DisposableEffect—are explained with Kotlin code examples to help Android developers build robust, reactive UI components.
Jetpack Compose is a new toolkit for building native Android UI; effective state management is crucial for creating robust, efficient, and maintainable applications.
remember + MutableState
Using remember stores an object in memory; the value computed by remember is kept across recompositions. mutableStateOf creates an observable MutableState that triggers recomposition when its value changes.
There are three ways to declare a MutableState inside a composable:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }Counter example:
@Composable
fun Counter() {
var count by remember { mutableStateOf(1) }
Button(onClick = { count++ }) {
Text(text = count.toString())
}
}List with click‑effect example:
@Composable
fun List(dataList: List
) {
val selectedStates = remember {
dataList.map { mutableStateOf(false) }
}
LazyColumn(content = {
itemsIndexed(dataList) { index, s ->
val isSelect = selectedStates[index]
Text(text = s, modifier = Modifier.selectable(
selected = isSelect.value,
onClick = { isSelect.value = !isSelect.value }
))
}
})
}remember can also accept a key parameter; when the key changes, the cached value is invalidated and recomputed.
@Composable
inline fun
remember(
key1: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}MutableState + ViewModel
mutableStateOf can be used directly in a ViewModel ; it is thread‑safe and notifies composables when updated. In a ViewModel you do not need remember because the ViewModel itself retains state across configuration changes.
class MainViewModel : ViewModel() {
var count by mutableStateOf(1)
private set
fun increase() {
count++
}
} @Composable
fun Counter() {
val viewModel: MainViewModel = viewModel()
Button(onClick = { viewModel.increase() }) {
Text(text = viewModel.count.toString())
}
}The viewModel() function requires the following dependency:
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")StateFlow State Management
StateFlow is typically combined with a ViewModel ; composables collect the flow with collectAsState to achieve reactive UI updates.
class MainViewModel : ViewModel() {
private val _dataFlow = MutableStateFlow
>(emptyList())
val dataFlow: StateFlow
> = _dataFlow
fun getData() {
val dataList = arrayListOf("hello", "google", "android", "apple", "ios", "huawei", "harmony")
_dataFlow.value = dataList
}
}Button to fetch data:
@Composable
fun List() {
val viewModel: MainViewModel = viewModel()
val dataList by viewModel.dataFlow.collectAsState()
Column {
Button(onClick = { viewModel.getData() }) {
Text(text = "GetData")
}
LazyColumn(content = {
items(dataList) {
Text(text = it)
}
})
}
}LiveData State Management
LiveData is a lifecycle‑aware observable data holder; it is usually used with a ViewModel and converted to Compose state via observeAsState .
Dependency:
implementation("androidx.compose.runtime:runtime-livedata:1.4.0")Similar code to the StateFlow example, adapted for LiveData:
class MainViewModel : ViewModel() {
private val _liveData = MutableLiveData
>(emptyList())
val liveData: LiveData
> = _liveData
fun getData() {
val dataList = arrayListOf("hello", "google", "android", "apple", "ios", "huawei", "harmony")
_liveData.value = dataList
}
} @Composable
fun List() {
val viewModel: MainViewModel = viewModel()
val dataList by viewModel.liveData.observeAsState(emptyList())
Column {
Button(onClick = { viewModel.getData() }) {
Text(text = "GetData")
}
LazyColumn(content = {
items(dataList) {
Text(text = it)
}
})
}
}State Lifting
State lifting moves state up to a parent composable and passes it down as parameters, ensuring a single source of truth and improving reusability.
@Composable
fun Counter() {
var count by remember { mutableStateOf(1) }
CounterPage(count = count) { count++ }
}
@Composable
fun CounterPage(count: Int, increase: () -> Unit) {
Button(onClick = increase) {
Text(text = count.toString())
}
}Benefits of state lifting:
Centralized state management : easier to track and manage.
State consistency : all components share the same state.
Reusability : child components become more generic.
Logic separation : UI logic is separated from state logic.
Side Effects
Side effects occur outside the composable scope; Jetpack Compose provides several Effect APIs such as LaunchedEffect , rememberCoroutineScope , rememberUpdatedState , DisposableEffect , SideEffect , produceState , derivedStateOf , snapshotFlow , and non‑restartable effects.
LaunchedEffect
Runs a suspend function within the composable scope; starts a coroutine when the composable enters the composition and cancels it when it leaves.
@Composable
fun Effect() {
val count = remember { mutableStateOf(1) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
count.value++
}
}
Text(text = "${count.value}")
}rememberCoroutineScope
Provides a coroutine scope that can be used to launch coroutines from outside LaunchedEffect .
@Composable
fun Effect() {
val count = remember { mutableStateOf(1) }
val coroutineScope = rememberCoroutineScope()
Column {
Button(onClick = {
coroutineScope.launch {
delay(1000)
count.value++
}
}) {
Text(text = "${count.value}")
}
}
}rememberUpdatedState
Ensures the latest value is captured by an effect without restarting the effect when the value changes.
@Composable
fun DelayText(text: String) {
var delayText by remember { mutableStateOf("") }
val updateText by rememberUpdatedState(newValue = text)
LaunchedEffect(Unit) {
delay(3000)
delayText = updateText
}
Text(text = "DelayText: $delayText")
}DisposableEffect
Used for cleanup when a key changes or the composable leaves the composition; example shows a lifecycle observer handling start/stop events.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit,
onStop: () -> Unit
) {
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StateComposeAppTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column {
HomeScreen(LocalLifecycleOwner.current, { /* onStart */ }, { /* onStop */ })
}
}
}
}
}
}SideEffect
Runs after every successful recomposition; useful for publishing Compose state to non‑Compose code.
@Composable
fun Counter() {
var count by remember { mutableStateOf(1) }
SideEffect {
// Called after each recomposition
handleCount(count)
}
Text(text = count.toString())
Button(onClick = { count++ }) {
Text(text = "increase")
}
}produceState
Starts a coroutine to convert non‑Compose state into Compose state, automatically triggering recomposition on updates.
var count = 0
@Composable
fun StateCounter() {
val countState = produceState(initialValue = count) {
while (true) {
delay(1000)
value++
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = countState.value.toString())
}
}derivedStateOf
Derives a new state from one or more existing states, recomputing only when the source states change.
@Composable
fun Counter() {
var count by remember { mutableStateOf(1) }
val beyondTen by remember { derivedStateOf { count > 10 } }
Column {
Text(text = "Does it exceed 10: $beyondTen")
Button(onClick = { count++ }) {
Text(text = count.toString())
}
}
}snapshotFlow
Converts a State object into a cold Flow that emits the current value when collected.
@Composable
fun Counter() {
var count by remember { mutableStateOf(1) }
val countFlow = snapshotFlow { count }
LaunchedEffect(Unit) {
countFlow.collect {
// Process the value here
}
}
Column {
Text(text = count.toString())
Button(onClick = { count++ }) {
Text(text = "increase")
}
}
}Restartable Effects
Effects such as LaunchedEffect , produceState , and DisposableEffect use keys to cancel and restart based on key changes; using a constant key makes the effect run only once.
@Composable
fun Counter() {
var count by remember { mutableStateOf(1) }
LaunchedEffect(count) {
// Executed each time count changes
}
Column {
Text(text = count.toString())
Button(onClick = { count++ }) {
Text(text = "increase")
}
}
}
@Composable
fun Counter() {
var count by remember { mutableStateOf(1) }
LaunchedEffect(true) {
// Executed only once
}
Text(text = count.toString())
Column {
Button(onClick = { count++ }) {
Text(text = "increase")
}
}
}Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.