CodeNewbie Community 🌱

Cover image for Adding dark mode to Android app in Jetpack compose: Part 1
Tristan
Tristan

Posted on

Adding dark mode to Android app in Jetpack compose: Part 1

Introduction

  • This is going to be a 3 part series where I show how to set up a dark mode with jetpack compose. The three parts are:

1) Setting up dark mode for single view
2) Setting up dark mode for entire app
3) Animating color transition

Github link

What we are building today

  • Today we are going to be making this dark mode toggle:

Android Dark View

Android Light View

Finding your colors

  • If you are like me, you probably don't know a lot about colors and design. Thankfully, Google has provided us with 2 tools to help with just that:

1) The color picker
2) The color display tool

  • After you have used those tools to find the appropriate colors for your app, create a new file called Colors.kt and add your choosen colors to it:
val primaryLight =Color(0xFFbfd5ef)
val primaryLightVariant =Color(0xFFf2ffff)
val lightSecondary = Color(0xFFefd8bf)
val lightSecondaryVariant = Color(0xFFefd8bf)

val Black2 = Color(0xFF000000)
val White2= Color(0xFFFFFFFF)
val RedErrorDark = Color(0xFFB00020)
val RedErrorLight = Color(0xFFEF5350)

val primaryDark =Color(0xFF102840)
val primaryDarkVariant =Color(0xFF00001a)
val darkSecondary = Color(0xFF402810)
val darkSecondaryVariant = Color(0xFF200000)

Enter fullscreen mode Exit fullscreen mode
  • The name of these colors are not super important but we will be using them when creating our theme

Creating the theme:

  • First of all, if you are unfamiliar with Theming in Jetpack compose. I would highly recommend you read the Custom Theming codelab and the replacing material systems article.

  • So Jetpack compose has an implementation of the Material Designs in a class called MaterialTheme. This type of implementation contains the Material designs, color, typography and shape attributes. When we customize these values they are automatically reflected in the Material components we use(like the Scaffold )

  • We need to create a new file called Theme.kt and place this code inside of it:

private val LightThemeColors = lightColors(
    primary = primaryLight,
    primaryVariant = primaryLightVariant,
    onPrimary = Black2,
    secondary = lightSecondary,
    secondaryVariant = lightSecondaryVariant,
    onSecondary = Black2,
    error = RedErrorDark,
    onError = RedErrorLight,

)

private val DarkThemeColors = darkColors(
    primary = primaryDark,
    primaryVariant = primaryDarkVariant,
    onPrimary = White2,
    secondary = darkSecondary,
    secondaryVariant = darkSecondaryVariant,
    onSecondary = White2,
    error = RedErrorLight,
    onError = RedErrorLight,
    //surface = Color(0xFF3c506b),


)

@Composable
fun AppTheme(
    darkTheme: Boolean,
    content: @Composable () -> Unit,
) {
    MaterialTheme(
        colors = if (darkTheme) DarkThemeColors else LightThemeColors,
        content= content
    )
}

Enter fullscreen mode Exit fullscreen mode
  • First I would like to draw our attention to the lightColors() and darkColors() functions. These are used to build our Material Design color system. We will use this to provide a sensible default so we don't always have to specify colors. Keen eyed viewers will notice that there is no surface or background parameter specified for the color functions. This is intentional on my part to demonstrate how they will be automatically generated for us. We are able to see the surface generation when we use the Scaffold composable later in this tutorial. The color of the Scaffolds drawer is determined by the surface color.

Replacing the default Material color system

  • Now to override the default color theme we use this code:
@Composable
fun AppTheme(
    darkTheme: Boolean,
    content: @Composable () -> Unit,
) {
    MaterialTheme(
        colors = if (darkTheme) DarkThemeColors else LightThemeColors,
        content= content
    )
}
Enter fullscreen mode Exit fullscreen mode
  • With the code above we are centralizing the styling by creating our own composable that wraps and configures a MaterialTheme. This gives us a single place to specify out theme customization and allows us to easily reuse it. The colors we use will be determined by the boolean value of darkTheme. In this tutorial the darkTheme value is stored inside of a viewModel.

Using our custom themes with Scaffold and Switch

  • Assuming we have a normal Scaffold like so:
val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()

    Scaffold(

        scaffoldState = scaffoldState,
        drawerGesturesEnabled = scaffoldState.drawerState.isOpen,
        topBar = {
            TopAppBar(
                title = { Text("Calf Tracker") },
                navigationIcon = {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.drawerState.open() }

                        }
                    ) {
                        Icon(Icons.Filled.Menu, contentDescription = "Toggle navigation drawer")
                    }
                }
            )
        },
        drawerContent = {
            SwitchDrawer()


        }

    )

Enter fullscreen mode Exit fullscreen mode
  • Now I will not be going into much default about the Scaffold. However, I will point out that everything inside the drawerContent, is going to be shown when the drawer is open and its color is determined by the surface parameter of the color functions we created earlier. The SwitchDrawer() is a composable that we have to create:
@Composable
fun SwitchDrawer(viewModel: WeatherViewModel = viewModel()){

    var switchState by remember { mutableStateOf(true) }

    Row(
        Modifier
            .fillMaxWidth()
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceEvenly


    ) {
        Text("Dark mode", style = TextStyle(fontSize = 18.sp),modifier = Modifier.weight(1f))

        Spacer(Modifier.width(8.dp))
        Switch(
            checked = switchState,
            onCheckedChange ={
                switchState=it
                viewModel.setDarkMode()
                             },//called when it is clicked
            colors = SwitchDefaults.colors(
                checkedThumbColor = MaterialTheme.colors.primary,
                uncheckedThumbColor = MaterialTheme.colors.primary,
                checkedTrackColor = MaterialTheme.colors.secondary,
                uncheckedTrackColor = MaterialTheme.colors.secondary,
            )
        )


    }
}

Enter fullscreen mode Exit fullscreen mode
  • Firstly, the switchState is a MutableState that the Switch component uses to determine what position it is(open or close). The onCheckedChange parameter is a callback that is called when the switch is clicked on:
onCheckedChange ={
                switchState=it
                viewModel.setDarkMode()
                     }

Enter fullscreen mode Exit fullscreen mode
  • As you can see we pass it a lambda expression and in this context the it is a boolean value determining if the switch has been clicked or not. The viewModel.setDarkMode() is the method stored in the viewModel that is used to trigger the actual color changes. It does so by simply changing a Boolean value over and over again

  • The colors parameter is used to display the colors of the actual switch:

colors = SwitchDefaults.colors(
                checkedThumbColor = MaterialTheme.colors.primary,
                uncheckedThumbColor = MaterialTheme.colors.primary,
                checkedTrackColor = MaterialTheme.colors.secondary,
                uncheckedTrackColor = MaterialTheme.colors.secondary,
            )

Enter fullscreen mode Exit fullscreen mode
  • Since we are using the MaterialTheme.colors, which we have overridden in our theme. The color of the Switch will change depending if we are in dark mode or not.

Using our Custom theme

  • So to actually use our custom themes we need to wrap our custom theme around the base composable like so:
AppTheme(viewModel.uiState.value.darkMode){
        ScaffoldView()
    }
Enter fullscreen mode Exit fullscreen mode
  • The viewModel.uiState.value.darkMode is just a boolean value stored inside a viewModel that gets toggled by viewModel.setDarkMode().

  • In order to actually make the colors change we must use the MaterialTheme:

Column(modifier = Modifier.background(MaterialTheme.colors.primary)){}
Enter fullscreen mode Exit fullscreen mode
  • Now as long as this Column is nested inside of our AppTheme anytime the switch is toggled, it will change the colors to be either light or dark, depending on what was defined inside the lightColors() and darkColors() functions.

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Top comments (1)

Collapse
 
lopiccoloson23 profile image
lopiccoloson23

Adding dark mode to an Android app using Jetpack Compose involves creating a seamless user experience that adjusts to different lighting conditions. In Part 1, you'll begin by configuring your theme to support dark mode, which requires defining dark color schemes and integrating them into your app’s UI elements. Jetpack Compose makes this process efficient with its MaterialTheme and darkColors functions, allowing for a dynamic and responsive design. By leveraging these tools, you ensure that your Android app not only looks good but also provides a user-friendly experience in both light and dark environments.