Mobile Development 15 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
State Management in Jetpack Compose: remember, MutableState, ViewModel, StateFlow, LiveData, and Effect APIs

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")
        }
    }
}
ViewModelAndroidstate-managementKotlinJetpack ComposeEffect APIMutableState
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.