Search code examples
androidkotlinandroid-themeandroid-jetpackandroid-jetpack-compose

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

  • 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
        }
    }