TL;DR change the theme and recompose the app between light and dark themes onClick.
Hello! I have an interesting issue I have been struggling to figure out and would love some help. I am trying to implement a settings screen which lets the user change the theme of the app (selecting Dark, Light, or Auto which matches system setting).
I am successfully setting the theme dynamically via invoking the isSystemInDarkTheme() function when choosing the color palette, but am struggling to recompose the app between light and dark themes on the click of a button.
My strategy now is to create a theme model which hoists the state from the settings component which the user actually chooses the theme in. This theme model then exposes a theme state variable to the custom theme (wrapped around material theme) to decide whether to pick the light or dark color palette. Here is the relevant code -->
Theme
@Composable
fun CustomTheme(
themeViewModel: ThemeViewModel = viewModel(),
content: @Composable() () -> Unit,
) {
val colors = when (themeViewModel.theme.value.toString()) {
"Dark" -> DarkColorPalette
"Light" -> LightColorPalette
else -> if (isSystemInDarkTheme()) DarkColorPalette else LightColorPalette
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes,
content = content
)
}
Theme model and state variable
class ThemeViewModel : ViewModel() {
private val _theme = MutableLiveData("Auto")
val theme: LiveData<String> = _theme
fun onThemeChanged(newTheme: String) {
when (newTheme) {
"Auto" -> _theme.value = "Light"
"Light" -> _theme.value = "Dark"
"Dark" -> _theme.value = "Auto"
}
}
}
Component (UI) code
@Composable
fun Settings(
themeViewModel: ThemeViewModel = viewModel(),
) {
...
val theme: String by themeViewModel.theme.observeAsState("")
...
ScrollableColumn(Modifier.fillMaxSize()) {
Column {
...
Card() {
Row() {
Text(text = theme,
modifier = Modifier.clickable(
onClick = {
themeViewModel.onThemeChanged(theme)
}
)
)
}
}
}
Thanks so much for your time and help! ***I have elided some code here in the UI component, it is possible I have left out some closure syntax in the process.
Dark theme In Compose, you implement light and dark themes by providing different sets of Colors to the MaterialTheme composable, and consuming colors through the theme:
A Material Theme comprises color , typography and shape attributes. When you customize these attributes, your changes are automatically reflected in the components you use to build your app. Jetpack Compose implements these concepts with the MaterialTheme composable:
One possibility, shown in the Jetpack theming codelab, is to set the darkmode via input parameter, which ensures the theme will be recomposed when the parameter changes: Show activity on this post.
If you plan to support light and dark app themes (explained shortly), try to select a color scheme which supports white text and a color scheme which supports black text. Example of light and dark color schemes. Create a file called something like Color.kt (the name does not matter) and fill it with immutable val ues:
One possibility, shown in the Jetpack theming codelab, is to set the darkmode via input parameter, which ensures the theme will be recomposed when the parameter changes:
@Composable
fun CustomTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
content = content
)
}
In your mainActivity you can observe changes to your viewModel and pass them down to your customTheme:
val darkTheme = themeViewModel.darkTheme.observeAsState(initial = true)
CustomTheme(darkTheme.value){
//yourContent
}
This way your compose previews can simply be styled in dark theme:
@Composable
private fun DarkPreview() {
CustomTheme(darkTheme = true) {
content
}
}
In case you want a button/switch to change the theme and make it persistent as setting, you can also achieve this by using Jetpack DataStore (recommended) or SharedPreferences, get your theme state in MainActivity and pass it to your Theme composable, and wherever you want to modify it.
You can find a complete working example with SharedPreferences
in this GitHub repo.
This example is using a Singleton
and Hilt for dependencies and is valid for all the preferences you want store.
Based on the docs, the official way to handle theme changes triggered by a user's action (ie. choice of a theme other than the system one through a custom built setting) is to use
AppCompatDelegate.setDefaultNightMode()
This call alone will take care of most things, including restarting any activity (thus, recomposing). For this to work, we need:
setContent
to extend AppCompatActvity
AppCompatDelegate
)CustomTheme
should also consider the value of the user's defaultNightMode
preference:@Composable
fun CustomTheme(
isDark: Boolean = isNightMode(),
content: @Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
content = content
)
}
@Composable
private fun isNightMode() = when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_NO -> false
AppCompatDelegate.MODE_NIGHT_YES -> true
else -> isSystemInDarkTheme()
}
this is nice to have as it avoids the need to get this value in an Activity just to pass it to the theme with CustomTheme(isDark = isDark)
.
This article goes through all of the above providing more details.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With