Dark mode on android with jetpack compose using dataStore with hilt
Introduction:
“Dark mode” is a feature that has been gaining popularity in recent years, as it reduces eye strain and improves battery life. With Jetpack Compose, developers can easily implement dark theme in their Android applications. In this article, we’ll explore how to create a dark theme in Android using Jetpack Compose, and the benefits it provides to users. We’ll also discuss the steps involved in creating a dark theme, and how it can be customized to suit your application’s needs. So, whether you’re a beginner or an experienced developer, this guide will help you create a sleek and stylish dark theme for your Android application.
The final result | Video
Step 1 — Update the gradle dependencies
Hilt and dataStore dependencies are available via Google’s new Maven repository, simply add it to the list of repositories in your main build.gradle file:
plugins {
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
In your app/build.gradle
file, add the dependencies for hilt and dataStore
plugins {
...
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
dependencies {
//hilt
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
// data store
implementation "androidx.datastore:datastore-preferences:1.0.0"
}
Step 2 — creata app class for HiltAndroidApp
@HiltAndroidApp
class Yourpplication:Application()
Step 3 — add app class to android manifest
<application
...
android:name=".YourApplication"
...
/>
Step 4 — create app module class
we need dependency injection to provide context for create dataStore
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
fun provideDataStoreUtil(@ApplicationContext context: Context):DataStoreUtil = DataStoreUtil(context)
}
Step 5 — create DataStoreUtil class to create preferences dataStore
class DataStoreUtil @Inject constructor(context: Context) {
val dataStore = context.dataStore
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("settings")
val IS_DARK_MODE_KEY = booleanPreferencesKey("dark_mode")
}
}
Step 6— create data class for ThemeState
data class ThemeState(val isDarkMode: Boolean)
Step 7 — create ThemeViewModel to handle dark mode state
@HiltViewModel
class ThemeViewModel @Inject constructor(dataStoreUtil: DataStoreUtil) : ViewModel() {
private val _themeState = MutableStateFlow(ThemeState(false))
val themeState: StateFlow<ThemeState> = _themeState
private val dataStore = dataStoreUtil.dataStore
init {
viewModelScope.launch(Dispatchers.IO) {
dataStore.data.map { preferences ->
ThemeState(preferences[IS_DARK_MODE_KEY] ?: false)
}.collect {
_themeState.value = it
}
}
}
fun toggleTheme() {
viewModelScope.launch(Dispatchers.IO) {
dataStore.edit { preferences ->
preferences[IS_DARK_MODE_KEY] = !(preferences[IS_DARK_MODE_KEY] ?: false)
}
}
}
}
Step 8 — create custom Switch to choose the mode you need
this CustomSwitch has a nice view you can see video below
@Composable
fun CustomSwitch(
width: Dp = 45.dp,
height: Dp = 30.dp,
checkedTrackColor: Color = Color(0xFF50B384).copy(alpha = 0.7f),
uncheckedTrackColor: Color = Color(0xFFe0e0e0),
gapBetweenThumbAndTrackEdge: Dp = 4.dp,
borderWidth: Dp = 2.dp,
cornerSize: Int = 50,
iconInnerPadding: Dp = 4.dp,
thumbSize: Dp = 20.dp,
themeViewModel: ThemeViewModel = hiltViewModel(),
) {
// this is to disable the ripple effect
val interactionSource = remember {
MutableInteractionSource()
}
val themeState by themeViewModel.themeState.collectAsState()
// for moving the thumb
val alignment by animateAlignmentAsState(
if (themeState.isDarkMode
) 1f else -1f
)
Box(
modifier = Modifier
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow
)
), contentAlignment = Alignment.CenterEnd
) {
Text(
text = if (themeState.isDarkMode
) "ON" else "OFF",
fontFamily = Ubuntu,
fontWeight = FontWeight.SemiBold,
modifier = Modifier,
)
}
SpacerHorizontal16()
// outer rectangle with border
Box(
modifier = Modifier
.size(width = width, height = height)
.border(
width = borderWidth,
color = if (themeState.isDarkMode
) checkedTrackColor else uncheckedTrackColor,
shape = RoundedCornerShape(percent = cornerSize)
)
.clickable(
indication = null,
interactionSource = interactionSource
) {
themeViewModel.toggleTheme()
},
contentAlignment = Alignment.Center
) {
// this is to add padding at the each horizontal side
Box(
modifier = Modifier
.padding(
start = gapBetweenThumbAndTrackEdge,
end = gapBetweenThumbAndTrackEdge
)
.fillMaxSize(),
contentAlignment = alignment
) {
// thumb with icon
Icon(
if (themeState.isDarkMode
) painterResource(id = R.drawable.night_mode) else painterResource(id = R.drawable.light_mode),
contentDescription = if (themeState.isDarkMode) "Enabled" else "Disabled",
modifier = Modifier
.size(size = thumbSize)
.background(
color = if (themeState.isDarkMode) checkedTrackColor else uncheckedTrackColor,
shape = CircleShape
)
.padding(all = iconInnerPadding),
tint = Color.Black
)
}
}
// gap between switch and the text
Spacer(modifier = Modifier.height(height = 16.dp))
}
Step 9 — update theme from view model theme
@Composable
fun ComposeCourseTheme(
themeViewModel: ThemeViewModel = hiltViewModel(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val themeState by themeViewModel.themeState.collectAsState()
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (themeState.isDarkMode) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
themeState.isDarkMode -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = Color.Transparent.toArgb()
// WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
Now you can switch theme as you like and save theme state on dataStore