Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 465
Nicolas Mage Avatar asked Dec 08 '20 02:12

Nicolas Mage


People also ask

How to implement light and dark themes in compose?

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:

What is a material theme in jetpack?

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:

How to set darkmode in jetpack theme?

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.

How do I support light and dark App Themes?

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:


3 Answers

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
    }
}
like image 135
jns Avatar answered Oct 19 '22 03:10

jns


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.

like image 4
Stefano Sansone Avatar answered Oct 19 '22 05:10

Stefano Sansone


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 setContent to extend AppCompatActvity
  • 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 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.

like image 2
Marino Avatar answered Oct 19 '22 04:10

Marino