TLDR: our previews always have a white background,...
# compose-android
k
TLDR: our previews always have a white background, even when
uiMode
is set to
UI_NIGHT_MODE_YES
unless we start the preview with
Scaffold
or
Surface
. There must be a better way. Regarding
CompositionLocalProvider
, does anyone have any insight into this SO comment I just made? Basically, it's cool that we can customize/override
LocalContentColor
, but the next thing we reached for (to try to get our previews to look right) was something like
LocalBackgroundColor
or
LocalContainerColor
, etc.
Is the generally accepted practice that all previews start with something like `Scaffold`(whose background defaults to
color.background
) or
Surface
(whose background defaults to
color.surface
), or am I just missing something silly?
e
doesn't wrapping it in
MaterialTheme
set the composition locals to reasonable values?
y
Preview backgrounds are really inconsistent, especially between IDE previews and Device previews. Maybe worse for Wear. So I've found I always need some component with a background in order to not be unpleasantly surprised in some situation.
k
@ephemient ahh, forgot about starting the preview with
MaterialTheme
. That's the idea, you meant, right?
e
yep, using
Copy code
@Preview
@Composable
fun Sample() {
    MaterialTheme {
        Surface {
            // etc.
k
@ephemient well, in your example there, you're still putting
Surface
in there. That was what we were hoping to not have to do. But, additionally, it appears that using
MaterialTheme
in the preview does not change
LocalContentColor
, so no I don't think that's a solution.
e
oh I see, I didn't realize
MaterialTheme
lacked the right call… we have our own
CompanyNameTheme
wrapper that uses
isSystemInDarkTheme()
k
@ephemient indeed, we do the same:
Copy code
@Composable
fun SomePreview() {
    AcmeTheme {
        // Background is always pure white
        // and LocalContentColor is always
        // pure black
        someComposableToPreview()

        Surface {
            // Now because we're in a
            // Surface, the color behind
            // everything is
            // color.Surface and
            // LocalContentColor is not
            // pure black (because it is
            // onSurface). Would be 
            // nice to not have to do this
            someComposableToPreview()
        }
    }
}
s
All of our previews basically start with
Copy code
HedvigTheme {
  Surface(color = MaterialTheme.colorScheme.background) {
    // actual preview code
  }
}
Here’s an example https://github.com/HedvigInsurance/android/blob/878463340b9e94ad42efa1fd3a36a0e8d0[…]urance/step/terminationsuccess/TerminationSuccessDestination.kt The live template I use for this is:
Copy code
@com.hedvig.android.core.designsystem.preview.HedvigPreview
@androidx.compose.runtime.Composable
private fun Preview$NAME$() {
  com.hedvig.android.core.designsystem.theme.HedvigTheme {
    androidx.compose.material3.Surface(color = androidx.compose.material3.MaterialTheme.colorScheme.background) {
        $NAME$($END$)
    }
  }
}
So all I do is: • copy the name of the composable I want to preview. • write
prev
and press enter • paste my clipboard (this fills the preview name + the call inside the function) • press enter again And that’s it basically. This has been the easiest thing to do in our case.
c
Needing Surface and MaterialTheme for contentColor isn't necessarily a Preview problem. You see the need for these in Preview more because you are starting with very small, isolated Composables. You don't see it as much on device because well you are likely looking at a deeper Compose hierarchy that starts with MaterialTheme + Surface in setContent in your Activity
We’ve considered maybe creating some default scaffolding for all Previews in your project to use the same theme + surface, but that might not always be what you want. And in this case, you can do that already with your own Preview wrapper function that is yours to fully customize and compose.
In general though, this does call attention to the need for Surface basically every time you have Text, since without it contentColor for text doesn’t resolve properly. And this isn’t obvious considering Surface is in the Material package.
k
@Chris Sinco [G] for the time being, we've added
Copy code
// Theme.kt


CompositionLocalProvider(LocalContentColor provides ourColorScheme.ourDefaultContentColor) {
    MaterialTheme(
        colorScheme = ourColorScheme,
        ...
    )
}
So, can you imagine a world where
LocalContainerColor
(or similar) is a thing? That would fix ("fix") our previews all around, I believe.
c
Hmm I’m not sure I follow - are you trying to reduce the need to always have Theme+Surface in every Preview? What you are showing above + Surface can be put into it’s own Composable function, and then you can then use that to wrap every Compose hierarchy you have in a Preview to have consistent results.
In general, a convention I’ve seen emerge is that whenever you have Text in a Composable function, it’s best to include Surface somewhere in that hierarchy (usually at the root of the function). Then whenever you use this Composable in a Preview, as long as you have some theme, i.e. MaterialTheme, the background and foreground colors should look right. That way you don’t need to add additional Surface calls or the CompositionLocalProvider you have above to every Preview.
This works well, but I also realize it doesn’t seem ideal because it basically means you have to use Surface everywhere.
d
This is exactly what we do too, essentially we have a "PreviewSurface" that wraps around all this and a bit more actually - similar to Accompanist TestHarness it can take a few more parameters like fontScale etc - works out pretty good.
s
Is that something you can share dorche? Might be interesting to get some inspiration of how you use that. Might make my live template better 😅
k
@Chris Sinco [G] I should have added more context to my CompositionLocalProvider example. As of yesterday, we now have that in our Theme.kt file. It's really helpful for our previews which currently don't have
Surface
as their root and are light mode (because the preview's background is very light - if not pure white), so our light themed LocalContentColor is dark on light. Great. But in dark mode previews (where preview's background is that same light color), the light colored LocalContentColor is light on light and cannot be seen well, if at all. All in all, it was cool to discover we could use LocalContentColor very high up in our theme and that fixed the text color (and other content) in the previews to use our primary content color instead of the default black. It just left us reaching for
LocalContainerColor
so that it wouldn't always be required to wrap stuff in something like Scaffold or Surface in order to get the expected, default background color. Hopefully I'm making sense. Sorry if not.
c
@Kevin Worth gotcha. The background color thing can be odd without something like Surface in the Preview Compose hierarchy. By default we treat it as transparent, and so you end up seeing the background of the design canvas from Android Studio, which in theory makes sense in the IDE but not on device. That is because on device, even if you don’t have a container with a background in your hierarchy, your Compose app still sits in a Window, which has a background, which is themed.
When you use showBackground in Preview, I believe we add something to the hierarchy that uses MaterialTheme.colorScheme.background, In essence, showBackgroud serves a similar purpose as the Window background mentioned above
d
Yep sure, I just wanna note that we wrote this wrapper a year or so ago, there's every chance that there're neater ways now - I've just not had to revisit this and it does the job for us 🤷 FontScale and Density here work wonders when combined with Paparazzi + Showkase (working around some limitations) + FontScale and/or Density PreviewParameters (easy way to screenshot test different scales without forcing you to do it for absolutely every preview - you get to choose which ones actually need to be generated in 2x or so font scale) Edit2: TestHarness is just a wrapper around Density provider here, nothing fancy
c
It just left us reaching for
LocalContainerColor
so that it wouldn't always be required to wrap stuff in something like Scaffold or Surface in order to get the expected, default background color.
@Kevin Worth is this API necessary though if you have a theme Composable like MaterialTheme in your hierarchy, which should have container colors with LocalColors?
k
@Chris Sinco [G] indeed everything works great in the app, which we realized is because at the root of our activity we have a Scaffold, and then someone said, "Ok, so all of our composables we expect to be on the scaffold, we'll add Scaffold to the preview..." and that just didn't feel right. Regarding your comment about
showBackground
, we may be doing something wrong, but I've been unable to see a difference when toggling that back and forth. My theory has been perhaps my composable is covering up (with the preview not showing anything extra, outside of the previewed composable), but that's where I started to get confused, because like you said, I would think my composable should be transparent, and if it is, then the fact that I'm seeing white in the background tells me that something is going wrong somewhere, and maybe it's in something we're doing that's odd - still new to this project and figuring out what people did before me.
c
Interesting - let me try to repro. What build of Studio have you been seeing these issues with showBackground?
e
in Flamingo I'm seeing both previews show up as white boxes
Copy code
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun Sample() {
    Box(Modifier.size(20.dp, 20.dp))
}
this works as expected:
Copy code
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, backgroundColor = 0xFFFFFFFF, showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, backgroundColor = 0xFF000000, showBackground = true)
(not sure if that's the same as what Kevin is observing)
k
Also Flamingo, and yes the use of simply
showBackground = true
hasn't worked. Thanks @ephemient. Also I missed that
backgroundColor
was a possible parameter. So thanks for that too.
c
So I created this snippet that can help explain and visualize how Preview works in conjunction with its parameters (showBackground, backgroundColor) and the presence of MaterialTheme and Surface: https://gist.github.com/c5inco/28af7456b3abd67f5b49bd1cdf510a9b
These are the results. What it shows is Preview working as intended in that when you have
showBackground
but no Surface, we will insert a background that uses the default light/dark MaterialTheme.colorScheme.background color. When you don’t have
showBackground
it will be transparent, unless you use a Surface.
And in cases where you have MaterialTheme, you need to pass it light and dark colors (or your own colors based on whatever condition). MaterialTheme out of the box uses lightColors as the default parameter, and doesn’t know about dark mode, which is by design for the API.
And here’s the version when using the IDE in dark theme, so you can see which Previews have a transparent background
A thing to remember is that Preview doesn’t have context about your project. It operates on a single Composable function, isolated by design and so it will render what it’s given in its hierarchy, so you have to be explicit about what theme you are using, what background or surface it sits on, is it a screen or a component, state, etc. We have discussed possibly creating a way to configure a default context for all Previews in your project, but as I mentioned before, most use cases we’ve seen can be satisfied with a custom utility function that is used in every Preview, and you have full control over that and know your project context best.
In essence, Preview is designed in the same spirit as the rest of Compose: favor composability over configurability
k
@Chris Sinco [G] this is fantastic. Thank you so much for your time and attention. This gives me what I need to help us figure whether we're simply doing something wrong or if we just got ourselves confused. With all of that, I think I like the use of CompositionLocalProvider to provide LocalContentColor at the top of the theme, because then even your `PreviewWithThemeNoSurface`s would have the correct dark text on light, and light text on dark.
c
Yeah it definitely gives you more explicit control over the contentColor versus letting the Material logic calculate it for you based on usage of Surface, in all scenarios.
Though if you are using MaterialTheme, you should control content color via onSurface, onBackground, onPrimary, etc. And even if you don’t use Material specifically (like fully customized design system), following this kind of pairing does work well. Though many designers don’t think about it this way. 🤷‍♂️
c
Thanks @Chris Sinco [G] amazingly helpful. So for my case this is what I have as my theme
Copy code
ProvideMyAppColors(colors) {
    MaterialTheme(MaterialTheme.colors, content = content)
}
so I still need to add some conditional inside of
MaterialTheme(
to swtich between material light and dark colors, right? Because I'm using the above, and still getting a light background with dark preview.
Alright this seemed to do the trick
Copy code
if (isSystemInDarkTheme()) androidx.compose.material.darkColors() else androidx.compose.material.lightColors(),
Now that I'm using both m2 and m3 in my app (we're sorta in the midst of converting a bunch of things. I wonder if there's a way to do something like this
Copy code
ProvideMyAppColors(colors) {
    MaterialTheme(if (isSystemInDarkTheme()) androidx.compose.material.darkColors() else androidx.compose.material.lightColors()){
    androidx.material3.MaterialTheme(
        colorScheme =  if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(), content()
    )
}
}
ah yeah. that worked. wooooT
So my big mess up in this entire scenario is that I thought MaterialTheme.colors would know how to switch for light and dark. but yeah. everything is working as expected now.thanks chris
c
Yeah it doesn’t by default know about dark theme. It has a baseline dark color palette in the Material library for you to use, but it’s decoupled from the colorScheme/colors parameter by design. It’s why we try to show how to do handle dark theme in our project templates, since it’s not immediately obvious that MaterialTheme doesn’t handle that by default.
c
Gotcha. Not sure where I copied my code from. Ive been trying to use jetsnack for the best example so maybe i just didn't have dark mode handling for material because of that?
c
Possibly. Jetsnack should have dark theme handled. You might be referring to the snippets in our docs on Preview which need to be fixed
c
Maybe. Now that I know that this was the issue I will be vigilant about it and file issues. 😄
501 Views