When keep in mind() Does Not Bear in mind, Contemplate if()
One among my issues when Jetpack Compose was launched is its reliance on magic coming from
issues just like the Compose Compiler. Magic is fantastic for newcomers, because it reduces cognitive load.
Magic is ok for severe consultants (magicians), as for them it isn’t magic, however fairly
is sufficiently superior know-how. Magic generally is a drawback for these of us in between
these extremes, to the extent it makes it troublesome for us to know delicate habits
variations coming from small code modifications.
For instance, I’ve been utilizing Alex Styl’s ComposeTheme
just lately, to assist set up a non-Materials design system in Compose UI. The way in which that
you construct a theme with ComposeTheme is through a buildComposeTheme() top-level perform:
// TODO fantastic theme bits go right here
}
This returns a composable perform, which you’ll be able to apply akin to MaterialTheme():
enjoyable MainScreen() {
MyTheme {
BasicText(“Um, hello!”)
}
}
This works nicely.
I then added assist for gentle and darkish themes. Alex’s documentation reveals doing that
outdoors of the constructed theme perform:
enjoyable MainScreen() {
val MyTheme = if (isSystemInDarkTheme()) MyDarkTheme else MyLightTheme
MyTheme {
// use the theme, the place coloration references get mapped to gentle or darkish
}
}
Right here, MyDarkTheme() and MyLightTheme() are created utilizing buildComposeTheme(), simply
with totally different colours. We select which one to make use of, then apply it to our content material.
I wished to cover the decision-making, so I didn’t want it sprinkled all through the
code (e.g., @Preview capabilities). So, I wrote my very own wrapper:
enjoyable MyTheme(content material: @Composable () -> Unit) {
if (isSystemInDarkTheme()) MyDarkTheme(content material) else MyLightTheme(content material)
}
This may very well be referred to as like MyTheme() was earlier than, routing to MyDarkTheme() or MyLightTheme()
as wanted.
And it labored… or so I believed.
The app opts out of all automated configuration change “destroy the exercise” habits
through android:configChanges. What occurs is that Compose UI recomposes, and we replace
the UI based mostly on the brand new Configuration, not considerably totally different than updating
the UI based mostly on the results of another type of information change.
What I seen was that whereas the app labored, if I modified the theme whereas the app was operating,
the whole lot would reset to the start. So, if I did some stuff within the app (e.g., navigated
in backside nav), then used the notification shade tile to activate/off darkish mode, the app
would draw the best theme, however my modifications could be undone (e.g., I might be again on the
default backside nav location).
Finally, after some debugging, I found that keep in mind() appeared to cease working. 😮
enjoyable MainScreen() {
MyTheme {
val uuid = keep in mind { UUID.randomUuid() }
BasicText(“Um, hello! My title is: $uuid”)
}
}
Right here, I keep in mind a generated UUID. That ought to survive recomposition. For many issues,
it might – I might rotate the display with out concern. But when I modified theme, I might get
a contemporary UUID.
🧐
A lot debugging later, I spotted the issue.
Let’s return to the MyTheme() implementation:
enjoyable MyTheme(content material: @Composable () -> Unit) {
if (isSystemInDarkTheme()) MyDarkTheme(content material) else MyLightTheme(content material)
}
Once I toggle darkish mode,
my use of isSystemInDarkTheme() triggers a recomposition. Let’s suppose that isSystemInDarkTheme()
initially returned false, then later returns true on the recomposition. The false
meant that my unique composition of MyTheme() went down the MyLightTheme() department.
The later recomposition takes me down the MyDarkTheme() department. Compose treats these
as separate compositions. MyTheme() is recomposing, however it’s doing so by discarding
the MyLightTheme() composition and creating a brand new MyDarkTheme() composition. It does
not matter whether or not content material would generate the identical composition nodes or not —
the change within the root from MyLightTheme() to MyDarkTheme() causes the swap in
compositions.
My uuid is within the content material lambda expression. After we get rid of the MyLightTheme()
composition and change to the MyDarkTheme() composition, we begin over with respect
to the keep in mind() name, and I wind up with a contemporary random UUID.
One workaround is to “raise the if”, mixing Alex’s unique method with mine:
enjoyable MyTheme(content material: @Composable () -> Unit) {
val theme = if (isSystemInDarkTheme()) MyDarkTheme else MyLightTheme
theme(content material)
}
This does the identical factor, however Compose treats this as a single modified composition, and
the keep in mind() is retained. To be sincere, I’m not utterly clear why this workaround
works. That is nonetheless magic to me, although I’m sure that there are others for whom the
reasoning is evident.
That is the type of factor that we have now to be careful for when working in Compose. Compose
is a principled framework, however the Precept of Least Shock is just not at all times adopted…
at the least for these amongst us who usually are not magicians.
— Sep 13, 2024