android understanding the differences between Stateflow, mutableStateOf, rememberSavable
In Jetpack Compose, both mutableStateOf and rememberSaveable are used to manage state, but they operate on completely different levels of the Android lifecycle.
The Problem: Recomposition Wipes the Slate Clean
In traditional Android (XML), a UI element like an EditText stays on the screen forever, holding onto its own text.
In Jetpack Compose, UI elements are just functions. When data changes, Compose runs those functions all over again from the top to draw the updated screen. This process is called Recomposition.
The intention is to trigger an update to the UI if anything changes. mutableStateOf makes your data reactive (updates the UI), while rememberSaveable protects that data from being wiped out when the user rotates the screen or switches apps.
1. remember
In Jetpack Compose, remember is a built-in memory guard. Its sole job is to protect a variable from being destroyed and reset when your UI redraws itself.
To understand why it is absolutely necessary, you first have to understand how Compose works under the hood.
Look at this code without remember:
@Composable
fun CounterWithoutRemember() {
// ❌ THIS WILL NOT WORK
var count = 0
Button(onClick = { count++ }) {
Text("Clicks: $count")
}
}
Every single time you click that button, count++ happens, which forces the CounterWithoutRemember function to run again from the top. When it runs again, it encounters var count = 0 and resets the count back to 0. Your counter will never get past 1!
The Solution: remember
remember tells Compose: "Hey, the first time you run this function, execute the code inside my brackets and store the result in your internal memory cache. On future redraws, don't rerun that code—just give me back the cached value."
Here is the corrected version:
@Composable
fun CounterWithRemember() {
// THIS WORKS!
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicks: $count")
}
}
First Run (Initial Composition): Compose allocates a slot in its memory, runs
mutableStateOf(0), and stores it.Button Clicked:
countbecomes 1. Compose triggers a redraw (recomposition).Second Run (Recomposition): Compose encounters the
rememberblock. Instead of executingmutableStateOf(0)again, it looks into its memory cache and pulls out the current value (1).
Key Rules of remember
It is tied to the UI Lifecycle: As long as your Composable stays on the screen,
rememberkeeps its cache. If the user navigates away to a completely different screen and comes back, the memory is cleared, and it starts over.It DOES NOT survive Configuration Changes: If the user rotates the screen, changes the system language, or enters dark mode, the entire Activity is destroyed and rebuilt.
rememberwill lose its memory here. (That is when you upgrade torememberSaveable).
2. rememberSaveable
To understand rememberSaveable, we have to look at what happens when remember hits its absolute limit: Configuration Changes.
As we just established, remember keeps your data safe when the UI refreshes (recomposes). But it has a major blind spot.
The Problem: Screen Rotations Destroy the App's Memory
When a user rotates their phone, switches from light to dark mode, or changes the system language, Android doesn't just refresh the UI—it completely destroys the current Activity and rebuilds it from scratch.
Because remember stores its cache inside the lifecycle of that specific UI composition, destroying the Activity wipes that cache clean.
Look at this code with only remember:
fun NameInput() {
// ❌ THIS WILL LOSE DATA ON ROTATION
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Type your name") }
)
}
If a user types "Alexander" into this box, and then rotates their phone to landscape mode, the screen blinks, the Activity restarts, and the text box resets to blank "". This is a frustrating user experience.
The Solution: rememberSaveable
rememberSaveable does everything remember does, but it adds a survival mechanism. It hooks into Android’s permanent Saved Instance State framework.
When the phone rotates, rememberSaveable automatically takes your variable, bundles it up, and saves it to the Android system's temporary storage disk before the app destroys itself. When the app restarts a millisecond later, it pulls that data back out.
Here is the fixed version:
@Composable
fun NameInput() {
// THIS SURVIVES ROTATION!
var text by rememberSaveable { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Type your name") }
)
}
How the Magic Works Step-by-Step
User Types: The user types "Alexander". The state updates.
Screen Rotates: Android tells the app, "We are rebuilding."
The Save Phase:
rememberSaveablesteps in, grabs the string"Alexander", and hands it to Android'sBundlestorage.The Destruction: The screen is torn down. Normal
rememberblocks are completely erased from memory.The Resurrection: The screen is rebuilt in landscape mode.
rememberSaveablelooks at the systemBundle, sees"Alexander", and injects it right back into the variable before the user even notices.
The Fine Print: What can it save?
Because rememberSaveable has to write your data to a temporary system bundle, it cannot save just anything.
Automatically Supported: Primitives (
String,Int,Boolean,Float, etc.) and Lists/Arrays of primitives.Not Supported out of the box: Your custom data classes (e.g., a
User(name: String, age: Int)class).
If you try to put a custom data class into rememberSaveable, your app will crash on rotation. To fix that, you have to tell it how to save it by marking your data class with @Parcelize:
import kotlinx.parcelize.Parcelize
import android.os.Parcelable
@Parcelize
data class User(val name: String, val age: Int) : Parcelable
// Now this is completely safe!
var currentUser by rememberSaveable { mutableStateOf(User("Alex", 25)) }
Summary Analogy
If remember is an actor memorizing a line for a play on stage:
rememberworks great during the show. But if a fire alarm goes off and everyone has to evacuate the theater (Configuration Change), the actor panics, leaves, and forgets everything.rememberSaveableis an actor who writes their lines down in a notebook and puts it in their pocket before escaping. When they are let back into the theater, they open the notebook and pick up exactly where they left off.
3. mutableStateOf
To truly understand mutableStateOf, we have to look at the absolute core rule of Jetpack Compose: The UI is entirely driven by data.
As we just learned, remember and rememberSaveable are the memory vaults that protect your data. But mutableStateOf is the alarm system that tells the UI when that data has actually changed.
The Problem: Standard Variables are Blind and Deaf
In standard programming, when you change a variable's value, the computer knows it changed, but the screen has no idea.
Look at this code using a normal Kotlin variable (even with remember):
@Composable
fun LightSwitch() {
// ❌ THE UI WILL NEVER UPDATE
var isLightOn by remember {
var normalVariable = false
normalVariable
}
Column {
// This will print "The light is OFF" and NEVER change
Text(text = if (isLightOn) "The light is ON" else "The light is OFF")
Button(onClick = { isLightOn = !isLightOn }) {
Text("Flip Switch")
}
}
}
If you run this code and click the button, isLightOn successfully flips from false to true in the background. However, nothing happens on the screen. Why? Because Compose doesn't know you touched the variable. It has no way of knowing it needs to redraw the screen.
The Solution: mutableStateOf
mutableStateOf wraps your data inside a special object called a MutableState.
This object acts like a megaphone. The exact millisecond you change the value inside a MutableState, it screams out to Compose: "Hey! The data just changed! Drop everything you're doing and redraw any UI components that are currently looking at me!"
This automatic redrawing process is called Recomposition.
Here is the fixed version:
@Composable
fun LightSwitch() {
// THIS WORKS FLUSH!
var isLightOn by remember { mutableStateOf(false) }
Column {
// Compose "subscribes" to isLightOn right here
Text(text = if (isLightOn) "The light is ON" else "The light is OFF")
Button(onClick = { isLightOn = !isLightOn }) {
Text("Flip Switch")
}
}
}
The Magic Under the Hood: The Subscription
The Read (Subscription): When Compose draws the
Text()composable, it notices that you readisLightOn. Compose secretly writes down a note: "The Text component is watchingisLightOn."The Write (The Trigger): You click the button, executing
isLightOn = !isLightOn.The Alarm:
mutableStateOfdetects the change and alerts Compose.The Recomposition: Compose looks at its notes, sees that the
Textcomponent cares about this variable, and instantly re-runs just that part of the code to show the updated text.
Why do we write by remember { mutableStateOf(...) }?
Now that you know what all three do, you can see how they form the ultimate team. They handle two completely different jobs:
mutableStateOfhandles Reactivity (making the UI change when data changes).remember(orrememberSaveable) handles Persistence (making sure that data isn't wiped out when the function re-runs).
If you use mutableStateOf without remember, your variable triggers a redraw, but during that redraw, the variable resets to its starting value. You need both!
Summary Analogy
Think of your Composable UI like a smart dashboard in a high-tech car.
A normal variable is like a thermometer sitting inside the engine glovebox. The temperature is changing, but the driver can't see it.
mutableStateOfis like connecting that thermometer directly to a glowing digital screen on the dashboard. The very second the engine heat fluctuates by even one degree, the dashboard flashes the new number to the driver instantly.
4. Stateflow
To understand StateFlow, we have to step completely out of the UI layer.
While mutableStateOf, remember, and rememberSaveable are all tools designed to live inside your UI screens, StateFlow is a powerhouse designed to live in your architectural backend—most commonly inside your ViewModel.
The Problem: UI Tools are Trapped in the UI
Imagine you are building a production-ready app. You don't want your network calls, database queries, and heavy business logic mixed into your layout code. You want that logic in a separate class, like a ViewModel.
But look what happens if you try to use mutableStateOf inside a regular, non-UI Kotlin class:
// ❌ NOT IDEAL IN CLEAN ARCHITECTURE
class UserViewModel : ViewModel() {
// This works, but it tightly couples your business logic to Jetpack Compose.
// If you ever want to share this code with an iOS app (KMP) or use it in
// background services, it becomes messy.
var userName = mutableStateOf("Guest")
}
Furthermore, what if your data is coming from a background thread as a stream of updates (like tracking a user's real-time GPS location or a chat room message stream)? mutableStateOf isn't built to handle asynchronous, multi-threaded background streams out of the box.
The Solution: StateFlow
StateFlow comes from pure Kotlin Coroutines (not the Compose library). It is a thread-safe, asynchronous data pipe that always holds a single current value and emits updates to anyone listening.
Because it is "pure Kotlin," it can live anywhere—in a background service, a data repository, or a ViewModel.
Here is how you set it up in a ViewModel:
class CounterViewModel : ViewModel() {
// 1. The private "Mutable" version (only the ViewModel can write to it)
private val _count = MutableStateFlow(0)
// 2. The public "Read-Only" version exposed to the UI
val count: StateFlow<Int> = _count.asStateFlow()
fun incrementCounter() {
_count.value++ // Updates the flow from a background thread safely
}
}
The Bridge: Connecting StateFlow to Compose
Because StateFlow doesn't know what Compose is, your UI cannot read it directly. You have to convert it into a Compose-readable State using a special extension function called collectAsStateWithLifecycle().
Here is how it looks on your screen:
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// THE BRIDGE: Converts StateFlow into Compose State safely!
val countState by viewModel.count.collectAsStateWithLifecycle()
Button(onClick = { viewModel.incrementCounter() }) {
Text("Count from ViewModel: $countState")
}
}
How the Magic Works Step-by-Step
The ViewModel holds the data: The
StateFlowin the ViewModel starts at0.The UI Listens (Collects):
collectAsStateWithLifecycle()hooks up a background listener to the flow.The Event: The user clicks the button, calling
viewModel.incrementCounter().The Stream Emits: The
StateFlowupdates its value to1and pushes it down the pipe.The Recomposition: The bridge (
collectAsStateWithLifecycle) catches the new value, turns it into a ComposemutableStateOfunder the hood, and your screen automatically redraws!
Why use collectAsStateWithLifecycle() instead of just collectAsState()?
Android apps get paused all the time (e.g., the user presses the home button or receives a phone call).
If you use regular collectAsState(), your UI keeps listening to the data pipe in the background, wasting battery and processing power.
collectAsStateWithLifecycle() automatically closes the pipe when the app goes into the background, and reopens it the exact moment the user brings the app back to the screen.
Summary Analogy
If mutableStateOf is a dashboard screen flashing the current engine heat to the driver:
StateFlowis the heavy-duty industrial wiring running from the engine block, through the firewall, and up to the dashboard. It safely transmits the volatile, fluctuating data across different compartments of the car (from the background data layer to the front-facing UI).
5. Sharedflow
To understand SharedFlow (often referred to in this context as SharedStateFlow or used interchangeably with it), we have to look at the difference between a State (something you are) and an Event (something that happens).
While StateFlow is designed to represent a persistent, ongoing condition (like a user's profile data or a loading screen state), SharedFlow is designed to handle one-off, fleeting actions that occur in the background and need to be sent to the UI exactly once.
The Problem: StateFlow Remembers Too Much
StateFlow always keeps its latest value in memory. If a new observer starts listening to a StateFlow, it instantly receives whatever that last value was.
This is terrible for one-off events like showing a Snackbar, playing a sound effect, or navigating to a new screen.
Look at what happens if you try to handle a popup notification using StateFlow:
// ❌ NOT IDEAL FOR EVENTS
class PaymentViewModel : ViewModel() {
val paymentStatus = MutableStateFlow<String?>(null)
fun processPayment() {
// ... payment succeeds ...
paymentStatus.value = "Payment Successful!"
}
}
The user hits "Pay". The payment succeeds.
paymentStatuschanges to"Payment Successful!".The UI sees this and pops up a success message.
The user rotates their phone.
Because the Activity restarts, the UI starts listening to
paymentStatusagain.StateFlowsays, "Oh, hello! My last known value was 'Payment Successful!', here you go!"The success message pops up a second time, confusing the user completely.
The Solution: SharedFlow
SharedFlow is a data pipe that does not store a persistent state. It transmits values like a radio broadcast. If you are tuned in when the song plays, you hear it. If you tune in a second late, you missed it—the radio station doesn't replay the song just for you.
Because it doesn't cache a permanent state, it is the perfect tool for handling background events that should only trigger once.
Here is how you set it up in a ViewModel:
class PaymentViewModel : ViewModel() {
// 1. The private mutable broadcast channel
private val _uiEvents = MutableSharedFlow<PaymentEvent>()
// 2. The public read-only stream
val uiEvents = _uiEvents.asSharedFlow()
fun processPayment() {
viewModelScope.launch {
// ... processing logic ...
// 3. Emit the event into the ether
_uiEvents.emit(PaymentEvent.ShowSuccessSnackbar)
}
}
}
sealed interface PaymentEvent {
object ShowSuccessSnackbar : PaymentEvent
}
The Bridge: Collecting Events Safely in Compose
Because these are actions and not UI states, you don't typically use collectAsStateWithLifecycle(). Instead, you listen to the stream inside a LaunchedEffect block so you can execute a one-time action (like showing a Snackbar or navigating).
Here is how it looks on your screen:
@Composable
fun PaymentScreen(viewModel: PaymentViewModel = viewModel()) {
val snackbarHostState = remember { SnackbarHostState() }
// THE BRIDGE: Listens to the radio broadcast safely
LaunchedEffect(key1 = true) {
viewModel.uiEvents.collect { event ->
when (event) {
is PaymentEvent.ShowSuccessSnackbar -> {
// This block executes EXACTLY ONCE per emission
snackbarHostState.showSnackbar("Payment Successful!")
}
}
}
}
// Standard UI code below...
}
How the Magic Works Step-by-Step
The Event Triggers: The user clicks "Submit Payment". The ViewModel executes
_uiEvents.emit(...).The Broadcast: The event travels down the
SharedFlowpipe.The Interception: The
LaunchedEffectblock catches the event in real-time and tells the screen to slide the Snackbar up.The Disappearance: The event passes through and evaporates. It is no longer kept in memory.
The Rotation Test: If the user rotates their phone, the screen rebuilds. The
LaunchedEffectstarts listening again, but because theSharedFlowis empty, no duplicate snackbar appears.
The Cheat Sheet: StateFlow vs SharedFlow
| Feature | StateFlow | SharedFlow |
| What it represents | A current condition (State). | An occurrence (Event). |
| Default Value? | Required. Must start with something. | No. Starts completely empty. |
| Memory Cache? | Always keeps the absolute latest value. | Keeps nothing (unless configured with a replay buffer). |
| UI Collection Tool | collectAsStateWithLifecycle() | LaunchedEffect { flow.collect { ... } } |
| Real World Example | UserProfileData, NetworkErrorState | ShowToast, NavigateToHomeScreen, PlayAudio |
Summary Analogy
If StateFlow is a digital whiteboard in a corporate office where the project status is written down for anyone to walk up and read at any time of day...
SharedFlow is the office intercom loudspeaker. If the boss speaks over the intercom saying "Free donuts in the breakroom!", the people sitting at their desks hear it and run for donuts. If an employee walks into the office ten minutes later, the intercom is silent. They don't hear the past announcement, and they don't get double donuts.
6. MutableStateflow
To understand MutableSharedFlow, we simply have to look at the relationship between a lock and its key.
In the previous answer, we looked at SharedFlow, which represents the read-only radio broadcast. MutableSharedFlow is the actual microphone inside the recording studio that allows you to talk into that broadcast.
The Problem: Preventing Chaos in Your Architecture
In clean Android development, you always want a Single Source of Truth. You want your ViewModel to control the data logic, and your UI layer to simply sit back and watch.
If you expose a raw MutableSharedFlow directly to your UI, any screen components could inject random events into your pipeline, bypassing your business logic entirely.
Look at this insecure code:
Kotlin// ❌ BAD ARCHITECTURE
class ChatViewModel : ViewModel() {
// Anyone who can see this variable can write data to it!
val chatEvents = MutableSharedFlow<String>()
}
@Composable
fun BadScreen(viewModel: ChatViewModel) {
Button(onClick = {
// The UI is directly manipulating the stream.
// This bypasses validation, logging, or network processing!
viewModel.chatEvents.tryEmit("Hacked message!")
}) { Text("Send") }
}
The Solution: The Mutable/Immutable Split
MutableSharedFlow gives you access to the .emit() and .tryEmit() functions, which let you push new events down the pipe.
To keep your app architecture clean, you keep the MutableSharedFlow hidden inside the ViewModel as a private variable (indicated by an underscore _). Then, you expose it to the outside world as a read-only, non-mutable SharedFlow using .asSharedFlow().
Here is the proper, secure setup:
Kotlinclass NotificationViewModel : ViewModel() {
// 1. Private & Mutable: Only this ViewModel can broadcast events
private val _notificationEvents = MutableSharedFlow<String>()
// 2. Public & Read-Only: The UI can listen, but it cannot inject events
val notificationEvents: SharedFlow<String> = _notificationEvents.asSharedFlow()
fun triggerAlert() {
viewModelScope.launch {
// 3. We use the Mutable version to push an event into the stream
_notificationEvents.emit("New update available!")
}
}
}
Key Superpowers of MutableSharedFlow
Because MutableSharedFlow is the engine configured behind the scenes, it has a few configuration knobs you can turn when you create it:
replay: By default, this is 0. If you change it to 1, it will cache the last event and play it back to any new listeners (making it behave a little bit more like a StateFlow).
extraBufferCapacity: This gives the pipe extra breathing room. If your background thread is pumping out events faster than your UI can process them, this buffer holds them in a queue so they don't get lost.
onBufferOverflow: Tells the flow what to do when the buffer fills up. You can tell it to pause the background thread (SUSPEND), drop the oldest event (DROP_OLDEST), or drop the newest event (DROP_LATEST).
Kotlin// Example of a highly customized MutableSharedFlow
val events = MutableSharedFlow<String>(
replay = 1,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
The Complete Flow Toolchain Matrix
Now that you've mastered the whole family, here is how the pieces click together:
Class Can read? Can write/emit? Where it lives Best used for... MutableStateFlowYes Yes Private in ViewModel Updating a state value (e.g., counter = 5) StateFlowYes No Public in ViewModel Exposing current screen data safely to UI MutableSharedFlowYes Yes Private in ViewModel Broadcasting a one-time event (e.g., play sound) SharedFlowYes No Public in ViewModel Letting the UI listen to events safely
Summary Analogy
If SharedFlow is the radio wave broadcast drifting through the air for car radios to pick up...
MutableSharedFlow is the physical mixing board and microphone sitting inside the radio station booth. It is the tool used by the radio host (the ViewModel) to generate the sound waves, adjust the volume buffers, and control exactly what gets pushed out to the public airwaves
Comments