remember and mutableStateOf
Module 4 left Mandi Bhav with a search field and a row of commodity chips that work until the phone turns sideways, and then forget everything. Before fixing that, you need to know exactly what is holding the search text now, because two separate things are, and they fail in two separate ways. This lesson takes the line var query by remember { mutableStateOf("") } apart, breaks each half on the emulator, and shows what each half costs you when it is missing.
What the screen holds now
Open ui/PricesScreen.kt. Since module 4 the top of PricesScreen reads:
// Module 5 moves this state into a ViewModel; until then it is lost on rotation.
var query by remember { mutableStateOf("") }
var selectedCommodity by remember { mutableStateOf<String?>(null) }
val commodities = commoditiesIn(prices)
val visible = filterPrices(prices, query, selectedCommodity)
Two values are state: data that changes while the screen is showing, and that the screen has to redraw for. query is what the farmer typed; selectedCommodity is the chip they tapped, or null for "All". Everything else on the screen is calculated from those two and the price list.
Each line does two jobs, with two different functions:
| Piece | Its one job | Without it |
|---|---|---|
mutableStateOf("") |
Makes a value Compose can watch: reading it inside a composable is recorded, and writing it schedules a recomposition of everything that read it | Changing the value never redraws anything |
remember { … } |
Keeps the object across recompositions, instead of running the block again each time the function runs | A fresh object, with the initial value, on every recomposition |
by |
Kotlin property delegation, so you write query instead of query.value |
Nothing breaks; you write .value |
by needs two imports, androidx.compose.runtime.getValue and androidx.compose.runtime.setValue, as module 3 noted. The table is the whole lesson in short. The rest is proving each row on the emulator, because the failures look nothing like their causes.
Break it: a plain var
Make a throwaway copy of the project for these experiments (module 2 explained why: the real project stays clean). In the copy, replace the query line with an ordinary Kotlin variable, and log what happens:
var query = ""
var selectedCommodity by remember { mutableStateOf<String?>(null) }
Log.d("State", "PricesScreen body, query = \"$query\", commodity = $selectedCommodity")
and in the text field:
onValueChange = {
query = it
Log.d("State", "typed \"$it\", query is now \"$query\"")
},
Run it, tap the search field and type "pune". On the emulator the field stayed empty, all nine cards stayed on screen, and Logcat (filter tag:State) showed:
D State : PricesScreen body, query = "", commodity = null
D State : typed "p", query is now "p"
D State : typed "pu", query is now "pu"
D State : typed "pun", query is now "pun"
D State : typed "", query is now ""
Read it carefully. The variable did change: query is now "pun". The function body ran once, at start-up, and never again. Compose re-runs a composable when state it read has changed, and a plain var is not state: Compose has no way to know it was written. So the screen went on showing what it drew the first time, and the field fell back to the value it had been given, an empty string, and reported that too.
This is the most common Compose bug among people coming from other UI toolkits, where you set a field and the screen updates. Nothing crashes. The code reads correctly. It does nothing.
Break it: state without remember
Now put the state back, but leave out remember:
var query by mutableStateOf("")
It compiles. Typing "pune" on the emulator logged:
D State : PricesScreen body, query = "", commodity = null
D State : typed "p", query is now "p"
D State : PricesScreen body, query = "", commodity = null
D State : typed "", query is now ""
D State : typed "n", query is now "n"
D State : PricesScreen body, query = "", commodity = null
D State : typed "", query is now ""
D State : typed "e", query is now "e"
D State : PricesScreen body, query = "", commodity = null
D State : typed "", query is now ""
This time the write did cause a recomposition: the body ran again after "p". But running the body ran mutableStateOf("") again, which built a brand-new state object holding "", and the screen read that. Every keystroke was thrown away by the recomposition it caused, and the "u" never appeared at all. The field showed nothing.
Lint catches this one. ./gradlew lintDebug failed with:
PricesScreen.kt:48: Error: Creating a state object during composition without using remember [UnrememberedMutableState from androidx.compose.runtime]
var query by mutableStateOf("")
~~~~~~~~~~~~~~
Android Studio underlines it in red as you type, too. Run lint before you believe a screen works; module 4 found the innerPadding mistake the same way.
Remember, working
Put the line back as it was, var query by remember { mutableStateOf("") }, keep the log lines, and type "pune" again:
D State : PricesScreen body, query = "", commodity = null
D State : typed "p", query is now "p"
D State : typed "pu", query is now "pu"
D State : PricesScreen body, query = "pu", commodity = null
D State : typed "pun", query is now "pun"
D State : PricesScreen body, query = "pun", commodity = null
D State : typed "pune", query is now "pune"
D State : PricesScreen body, query = "pune", commodity = null
The field shows "pune" and the list filters. Notice the first two keystrokes: "p" and "pu" arrived 10 ms apart and produced one recomposition, with query = "pu". A write does not recompose on the spot; it marks the screen as needing it, and Compose recomposes once before the next frame with whatever the latest value is. That is why writing state in a loop does not redraw the screen a hundred times.
remember stores the object in the composition, the tree Compose keeps of what it has drawn, at the position where this call sits. The next time PricesScreen runs, the same call at the same position gets the same object back, and the block is not run again. It stays there for as long as PricesScreen stays in the composition.
The trap: state that resets on rotation
Tap the "Onion" chip as well, so the screen shows only Pune onion. Then rotate the emulator (the rotate button in its toolbar, or adb shell settings put system user_rotation 1 with auto-rotate off, as module 2 did):
D State : PricesScreen body, query = "pune", commodity = Onion
D State : PricesScreen body, query = "", commodity = null
The landscape screen showed an empty search field, "All" selected, and every price in two columns. The user's search is gone, and nothing in the log says why.
You already know why from module 2. Rotation destroys the activity and creates a new one. setContent in the new activity's onCreate starts a new composition, and remember lives in the composition, so it starts again from mutableStateOf(""). remember survives recomposition. It does not survive the activity being recreated, and it certainly does not survive the process being killed.
A preview of the fix: rememberSaveable
Compose has a second function for exactly this. In the copy, change both lines:
var query by rememberSaveable { mutableStateOf("") }
var selectedCommodity by rememberSaveable { mutableStateOf<String?>(null) }
with the import androidx.compose.runtime.saveable.rememberSaveable. Type "pune", tap "Onion", rotate:
D State : PricesScreen body, query = "pune", commodity = Onion
D State : PricesScreen body, query = "pune", commodity = Onion
It survived. Then press Home, kill the process with adb shell am kill com.riztechacademy.mandibhav as module 2 did, and open the app again:
D State (20180): PricesScreen body, query = "pune", commodity = Onion
D State (20310): PricesScreen body, query = "pune", commodity = Onion
A new process id, and the search came back. rememberSaveable does everything remember does, and also writes the value into the saved-instance-state Bundle from module 2's log, the one Android keeps outside your process.
You have been using it already without knowing. rememberScrollState() in PricesScreen is built on rememberSaveable, which is why a scrolled list keeps its position when you rotate.
What it costs. Only types a Bundle can hold can be saved: strings, numbers, booleans and a few others, or a class you teach it to save. And everything you save is copied out of your process on every Home press and rotation, so it must stay small; lesson 4 shows the crash when it does not.
So why not stop here? Two reasons, and they are the next three lessons. The state lives inside PricesScreen, so the screen cannot be previewed or tested with a search already typed. And the filtering runs in the composable body, which module 3 warned against for anything larger than nine prices. Put both lines back to remember in the copy before lesson 2: the real project keeps remember until lesson 3 replaces it.
State types, as a reference
| Function | Holds | Use it for |
|---|---|---|
mutableStateOf(x) |
Any value | Strings, nullable values, your own immutable classes |
mutableIntStateOf(0) |
An Int, without boxing |
Counters; module 2's lifecycle demo used it |
mutableLongStateOf, mutableFloatStateOf, mutableDoubleStateOf |
That primitive | The same, for other number types |
mutableStateListOf<T>() |
A list whose adds and removes are watched | A list you change in place |
mutableStateMapOf<K, V>() |
A map, the same way | A map you change in place |
One trap hides in that table: mutableStateOf(mutableListOf<String>()) watches the reference to the list, not its contents. list.add("Onion") changes the contents and leaves the reference the same, so nothing recomposes. Either replace the whole list (list = list + "Onion") or use mutableStateListOf.
And the two remembers, with the column the next lessons fill in:
| Survives… | Recomposition | Rotation | Process death |
|---|---|---|---|
A plain var in a composable |
No | No | No |
mutableStateOf without remember |
No | No | No |
remember { mutableStateOf(…) } |
Yes | No | No |
rememberSaveable { mutableStateOf(…) } |
Yes | Yes | Yes |
Check your work
What mutableStateOf does on its own. Makes a value whose reads Compose records and whose writes schedule a recomposition.
What remember does on its own. Keeps an object across recompositions, so the block runs only the first time.
Why the plain var version showed an empty field while the log said query is now "pun". The variable changed, but it is not state, so nothing recomposed and the screen kept showing what it first drew.
Why the version without remember lost every keystroke. The write recomposed the screen, and the recomposition built a new state object holding "".
Which tool catches the missing remember. Lint, as UnrememberedMutableState, an error.
Why "p" and "pu" produced one recomposition. Writes mark the screen for recomposition; Compose recomposes once per frame with the latest value.
Why remember loses the search on rotation. It lives in the composition, and rotation recreates the activity, which starts a new composition.
What rememberSaveable adds. It also writes the value into the saved-instance-state Bundle, so it survives rotation and process death.
Practice 3. On the emulator the click logged chosen = [All], then [All, All], and the body never ran again: the state still holds the same list object, so as far as Compose can tell nothing was written. Replacing the list (chosen = chosen + "All") or using mutableStateListOf fixes it. (Test it on "All", not on a commodity chip. A commodity chip also changes selectedCommodity, which recomposes the screen anyway and hides the bug, which is exactly how this bug survives in real code.)
Practice 5. The pid in the log changes (20180 to 20310 in the run above) and the line still reads query = "pune", commodity = Onion. Nothing in your code saved or restored it; rememberSaveable put it in the Bundle, and Android handed the Bundle to the new process.
Practice 6. The app crashed as soon as the screen opened, before any Home press:
java.lang.IllegalArgumentException: MutableState containing CommodityPrice(commodity=Onion, market=Pune (Gultekdi) APMC, date=2026-11-07, minPrice=1000, maxPrice=2800, modalPrice=2200) cannot be saved using the current SaveableStateRegistry. The default implementation only supports types which can be stored inside the Bundle. Please consider implementing a custom Saver for this class and pass it as a stateSaver parameter to rememberSaveable().
rememberSaveable checks that a value can be saved when it is remembered, so the mistake shows up at once rather than on the day a user presses Home. The message names the fix, a custom Saver; the better fix is to save the price's market and commodity, two strings, and look the price up again.
Practice
- In a copy of the project, reproduce the plain
varexperiment and read the log. Then reproduce the missing-rememberone, and run./gradlew lintDebugto see the error. - Put
rememberback, type "pune", tap "Onion", and rotate. Confirm the log shows the reset. - Break it on purpose: add
var chosen by remember { mutableStateOf(mutableListOf<String>()) }to the copy and logchosenin the body. Change the "All" chip'sonClicktochosen.add("All")plus a log line, and nothing else. Tap "All" twice. Does the body run again? Explain, then fix it two ways. - Scroll the price list half way down, rotate, and check the scroll position survives. Say which
rememberfunction is responsible. - Change both lines to
rememberSaveable, rotate, then repeat module 2's process-death steps (Home,am kill, reopen). Compare the pids in the log. - Harder:
var saved by rememberSaveable { mutableStateOf<CommodityPrice?>(null) }compiles. Addif (saved == null) saved = SamplePrices.puneOnionunder it, run the app, and read Logcat. Predict the result before you run it, from what this lesson said about what aBundlecan hold. Put everything back torememberwhen you finish.
Official documentation
- State and Jetpack Compose —
remember,mutableStateOf, the other observable types, and the warning about mutable collections. - Save UI state in Compose —
rememberSaveable, custom savers, and where each kind of state should be saved. - Lifecycle of composables — How a composable enters and leaves the composition, which is where
rememberkeeps its values.
Next: state hoisting, so that one owner holds the search and every composable below it is stateless.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.
About the internship