'Jetpack Compose: How to change theme from light to dark mode programmatically onClick
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.
Solution 1:[1]
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
}
}
Solution 2:[2]
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.
Solution 3:[3]
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:
- The Activity which calls
setContentto extendAppCompatActvity - The user's choice to be persisted and applied at each start of the app (through
AppCompatDelegate) - To define whether dark mode is enabled, your
CustomThemeshould also consider the value of the user'sdefaultNightModepreference:
@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.
Solution 4:[4]
It might not be the recommended way, but one option is to use the recreate method (available since API level 11).
In order to use it outside of the activity and within your composable, you could pass the function call. Taking your code as a basis
class SomeActivity {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
...
themeViewModel.onRecreate = { recreate() }
CustomTheme(themeViewModel) {
...
}
}
}
}
In your viewModel
class ThemeViewModel: ViewModel() {
lateinit var onRecreate: () -> Unit
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"
}
onRecreate
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | jns |
| Solution 2 | |
| Solution 3 | |
| Solution 4 | flyakite |
