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:
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)
- 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
)
}
- First I would like to draw our attention to the
lightColors()
anddarkColors()
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 nosurface
orbackground
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 thesurface
generation when we use theScaffold
composable later in this tutorial. The color of the Scaffolds drawer is determined by thesurface
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
)
}
- 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 thedarkTheme
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()
}
)
- 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 thesurface
parameter of the color functions we created earlier. TheSwitchDrawer()
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,
)
)
}
}
- Firstly, the
switchState
is aMutableState
that the Switch component uses to determine what position it is(open or close). TheonCheckedChange
parameter is a callback that is called when the switch is clicked on:
onCheckedChange ={
switchState=it
viewModel.setDarkMode()
}
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. TheviewModel.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 againThe 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,
)
- 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()
}
The
viewModel.uiState.value.darkMode
is just a boolean value stored inside a viewModel that gets toggled byviewModel.setDarkMode()
.In order to actually make the colors change we must use the MaterialTheme:
Column(modifier = Modifier.background(MaterialTheme.colors.primary)){}
- 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 thelightColors()
anddarkColors()
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)
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.