Dark mode on android with jetpack compose using dataStore with hilt

Ahmed Khater
4 min readApr 24, 2023

--

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.

this is final result to switch theme

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

Follow for more

--

--