When remember() Does Not Remember, Consider if()
One of my concerns when Jetpack Compose was released is its reliance on magic coming fromthings like the Compose Compiler. Magic is wonderful for newcomers, as it reduces cognitive load.Magic is fine for serious experts (magicians), as for them it is not magic, but ratheris sufficiently advanced technology. Magic can be a problem for those of us in betweenthose extremes, to the extent it makes it difficult for us to understand subtle behaviordifferences coming from small code changes.
For example, I have been using Alex Styl���s ComposeThemerecently, to help organize a non-Material design system in Compose UI. The way thatyou build a theme with ComposeTheme is via a buildComposeTheme() top-level function:
val MyTheme = buildComposeTheme { // TODO wonderful theme bits go here}This returns a composable function, which you can apply akin to MaterialTheme():
@Composablefun MainScreen() { MyTheme { BasicText("Um, hi!") }}This works well.
I then added support for light and dark themes. Alex���s documentation shows doing thatoutside of the constructed theme function:
@Composablefun MainScreen() { val MyTheme = if (isSystemInDarkTheme()) MyDarkTheme else MyLightTheme MyTheme { // use the theme, where color references get mapped to light or dark }}Here, MyDarkTheme() and MyLightTheme() are created using buildComposeTheme(), justwith different colors. We choose which one to use, then apply it to our content.
I wanted to hide the decision-making, so I didn���t need it sprinkled throughout thecode (e.g., @Preview functions). So, I wrote my own wrapper:
@Composablefun MyTheme(content: @Composable () -> Unit) { if (isSystemInDarkTheme()) MyDarkTheme(content) else MyLightTheme(content)}This could be called like MyTheme() was before, routing to MyDarkTheme() or MyLightTheme()as needed.
And it worked��� or so I thought.
The app opts out of all automatic configuration change ���destroy the activity��� behaviorvia android:configChanges. What happens is that Compose UI recomposes, and we updatethe UI based on the new Configuration, not significantly different than updatingthe UI based on the result of some other sort of data change.
What I noticed was that while the app worked, if I changed the theme while the app was running,everything would reset to the beginning. So, if I did some stuff in the app (e.g., navigatedin bottom nav), then used the notification shade tile to turn on/off dark mode, the appwould draw the right theme, but my changes would be undone (e.g., I would be back at thedefault bottom nav location).
Eventually, after some debugging, I discovered that remember() seemed to stop working. ����
@Composablefun MainScreen() { MyTheme { val uuid = remember { UUID.randomUuid() } BasicText("Um, hi! My name is: $uuid") }}Here, I remember a generated UUID. That should survive recomposition. For most things,it would ��� I could rotate the screen without issue. But if I changed theme, I would geta fresh UUID.
����
Much debugging later, I realized the problem.
Let���s go back to the MyTheme() implementation:
@Composablefun MyTheme(content: @Composable () -> Unit) { if (isSystemInDarkTheme()) MyDarkTheme(content) else MyLightTheme(content)}When I toggle dark mode,my use of isSystemInDarkTheme() triggers a recomposition. Let���s suppose that isSystemInDarkTheme()originally returned false, then later returns true on the recomposition. The falsemeant that my original composition of MyTheme() went down the MyLightTheme() branch.The later recomposition takes me down the MyDarkTheme() branch. Compose treats thoseas separate compositions. MyTheme() is recomposing, but it is doing so by discardingthe MyLightTheme() composition and creating a new MyDarkTheme() composition. It doesnot matter whether content would generate the same composition nodes or not ���the change in the root from MyLightTheme() to MyDarkTheme() causes the swap incompositions.
My uuid is in the content lambda expression. When we dispose of the MyLightTheme()composition and switch to the MyDarkTheme() composition, we start over with respectto the remember() call, and I wind up with a fresh random UUID.
One workaround is to ���lift the if���, blending Alex���s original approach with mine:
@Composablefun MyTheme(content: @Composable () -> Unit) { val theme = if (isSystemInDarkTheme()) MyDarkTheme else MyLightTheme theme(content)}This does the same thing, but Compose treats this as a single changed composition, andthe remember() is retained. To be honest, I am not completely clear why this workaroundworks. This is still magic to me, though I am certain that there are others for whom thereasoning is clear.
This is the sort of thing that we have to watch out for when working in Compose. Composeis a principled framework, but the Principle of Least Surprise is not always followed���at least for those among us who are not magicians.


