Skip to content

Commit 1700f77

Browse files
feat: Implement theme selection for the project
1 parent bfaf0a7 commit 1700f77

File tree

28 files changed

+709
-170
lines changed

28 files changed

+709
-170
lines changed

cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,7 @@
14381438
| | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*)
14391439
| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 (*)
14401440
| | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8 (*)
1441+
| | +--- project :core:model (*)
14411442
| | +--- project :core:common (*)
14421443
| | +--- project :core:datastore (*)
14431444
| | +--- project :feature:home
@@ -1527,6 +1528,7 @@
15271528
| | | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.7.0-rc01 (*)
15281529
| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 (*)
15291530
| | | +--- project :core:datastore (*)
1531+
| | | +--- project :core:model (*)
15301532
| | | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0 (*)
15311533
| | +--- org.jetbrains.compose.material3:material3:1.7.0-rc01 (*)
15321534
| | +--- org.jetbrains.compose.foundation:foundation:1.7.0-rc01 (*)

cmp-android/prodRelease-badging.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package: name='cmp.android.app' versionCode='1' versionName='2025.3.3-beta.0.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
1+
package: name='cmp.android.app' versionCode='1' versionName='2025.3.3-beta.0.4' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
22
sdkVersion:'26'
33
targetSdkVersion:'34'
44
uses-permission: name='android.permission.INTERNET'

cmp-navigation/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ kotlin {
1919
commonMain.dependencies {
2020
// Core Modules
2121
implementation(projects.core.data)
22+
implementation(projects.core.model)
2223
implementation(projects.core.common)
2324
implementation(projects.core.datastore)
2425

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
9+
*/
10+
package cmp.navigation
11+
12+
import androidx.lifecycle.ViewModel
13+
import androidx.lifecycle.viewModelScope
14+
import kotlinx.coroutines.flow.SharingStarted
15+
import kotlinx.coroutines.flow.StateFlow
16+
import kotlinx.coroutines.flow.map
17+
import kotlinx.coroutines.flow.onStart
18+
import kotlinx.coroutines.flow.stateIn
19+
import org.mifos.core.datastore.UserPreferencesRepository
20+
import org.mifos.core.model.DarkThemeConfig
21+
import org.mifos.core.model.ThemeBrand
22+
import org.mifos.core.model.UserData
23+
24+
class AppViewModel(
25+
settingsRepository: UserPreferencesRepository,
26+
) : ViewModel() {
27+
val uiState: StateFlow<AppUiState> = settingsRepository.userData
28+
.onStart {
29+
settingsRepository.getThemeBrand(ThemeBrand.DEFAULT)
30+
settingsRepository.getDarkThemeConfig(DarkThemeConfig.FOLLOW_SYSTEM)
31+
settingsRepository.getDynamicColorPreference(false)
32+
}
33+
.map { userDate ->
34+
AppUiState.Success(userDate)
35+
}
36+
.stateIn(
37+
scope = viewModelScope,
38+
started = SharingStarted.WhileSubscribed(5_000),
39+
initialValue = AppUiState.Loading,
40+
)
41+
}
42+
43+
sealed interface AppUiState {
44+
data object Loading : AppUiState
45+
data class Success(val userData: UserData) : AppUiState {
46+
override val shouldDisplayDynamicTheming = userData.useDynamicColor
47+
override val shouldUseAndroidTheme = when (userData.themeBrand) {
48+
ThemeBrand.DEFAULT -> false
49+
ThemeBrand.ANDROID -> true
50+
}
51+
52+
override fun shouldUseDarkTheme(isSystemInDarkTheme: Boolean): Boolean =
53+
when (userData.darkThemeConfig) {
54+
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme
55+
DarkThemeConfig.LIGHT -> false
56+
DarkThemeConfig.DARK -> true
57+
}
58+
}
59+
60+
val shouldDisplayDynamicTheming: Boolean get() = true
61+
val shouldUseAndroidTheme: Boolean get() = false
62+
fun shouldUseDarkTheme(isSystemInDarkTheme: Boolean) = isSystemInDarkTheme
63+
}

cmp-navigation/src/commonMain/kotlin/cmp/navigation/ComposeApp.kt

+14-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99
*/
1010
package cmp.navigation
1111

12+
import androidx.compose.foundation.isSystemInDarkTheme
1213
import androidx.compose.runtime.Composable
14+
import androidx.compose.runtime.getValue
1315
import androidx.compose.ui.Modifier
16+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1417
import androidx.navigation.compose.rememberNavController
1518
import cmp.navigation.navigation.RootNavGraph
1619
import org.koin.compose.koinInject
20+
import org.koin.compose.viewmodel.koinViewModel
1721
import org.mifos.core.data.utils.NetworkMonitor
1822
import org.mifos.core.data.utils.TimeZoneMonitor
1923
import org.mifos.core.designsystem.theme.MifosTheme
@@ -24,7 +28,16 @@ fun ComposeApp(
2428
networkMonitor: NetworkMonitor = koinInject(),
2529
timeZoneMonitor: TimeZoneMonitor = koinInject(),
2630
) {
27-
MifosTheme {
31+
val viewModel: AppViewModel = koinViewModel()
32+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
33+
34+
val isSystemInDarkTheme = isSystemInDarkTheme()
35+
36+
MifosTheme(
37+
darkTheme = uiState.shouldUseDarkTheme(isSystemInDarkTheme),
38+
androidTheme = uiState.shouldUseAndroidTheme,
39+
shouldDisplayDynamicTheming = uiState.shouldDisplayDynamicTheming,
40+
) {
2841
RootNavGraph(
2942
networkMonitor = networkMonitor,
3043
timeZoneMonitor = timeZoneMonitor,

cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
*/
1010
package cmp.navigation.di
1111

12+
import cmp.navigation.AppViewModel
13+
import org.koin.core.module.dsl.viewModelOf
1214
import org.koin.dsl.module
1315
import org.mifos.core.common.di.DispatchersModule
1416
import org.mifos.core.data.di.DataModule
@@ -24,10 +26,15 @@ object KoinModules {
2426
includes(DispatchersModule)
2527
}
2628

29+
private val AppModule = module {
30+
viewModelOf(::AppViewModel)
31+
}
32+
2733
val allModules = listOf(
2834
dataModule,
2935
dispatcherModule,
3036
DatastoreModule,
3137
SettingsModule,
38+
AppModule,
3239
)
3340
}

cmp-navigation/src/commonMain/kotlin/cmp/navigation/ui/App.kt

+14-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ import androidx.compose.material3.TopAppBarDefaults
3636
import androidx.compose.runtime.Composable
3737
import androidx.compose.runtime.LaunchedEffect
3838
import androidx.compose.runtime.getValue
39+
import androidx.compose.runtime.mutableStateOf
3940
import androidx.compose.runtime.remember
41+
import androidx.compose.runtime.saveable.rememberSaveable
42+
import androidx.compose.runtime.setValue
4043
import androidx.compose.ui.Modifier
4144
import androidx.compose.ui.composed
4245
import androidx.compose.ui.draw.drawWithContent
@@ -59,8 +62,8 @@ import org.mifos.core.designsystem.component.MifosNavigationBarItem
5962
import org.mifos.core.designsystem.component.MifosNavigationRail
6063
import org.mifos.core.designsystem.component.MifosNavigationRailItem
6164
import org.mifos.core.designsystem.icon.AppIcons
65+
import org.mifos.feature.settings.SettingsDialog
6266
import org.mifos.feature.settings.navigateToNotification
63-
import org.mifos.feature.settings.navigateToSettings
6467

6568
@Composable
6669
internal fun App(
@@ -78,6 +81,8 @@ internal fun App(
7881

7982
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
8083

84+
var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
85+
8186
// If user is not connected to the internet show a snack bar to inform them.
8287
val notConnectedMessage = stringResource(Res.string.not_connected)
8388
LaunchedEffect(isOffline) {
@@ -89,6 +94,12 @@ internal fun App(
8994
}
9095
}
9196

97+
if (showSettingsDialog) {
98+
SettingsDialog(
99+
onDismiss = { showSettingsDialog = false },
100+
)
101+
}
102+
92103
Scaffold(
93104
modifier = modifier,
94105
snackbarHost = { SnackbarHost(snackbarHostState) },
@@ -133,7 +144,8 @@ internal fun App(
133144
AppBar(
134145
title = stringResource(destination.titleText),
135146
onNavigateToSettings = {
136-
appState.navController.navigateToSettings()
147+
// appState.navController.navigateToSettings()
148+
showSettingsDialog = true
137149
},
138150
onNavigateToEditProfile = {},
139151
onNavigateToNotification = {

core/datastore/src/commonMain/kotlin/org/mifos/core/datastore/UserPreferencesRepository.kt

+13-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,19 @@
99
*/
1010
package org.mifos.core.datastore
1111

12-
import org.mifos.core.datastore.model.AppSettings
12+
import kotlinx.coroutines.flow.Flow
13+
import org.mifos.core.model.DarkThemeConfig
14+
import org.mifos.core.model.ThemeBrand
15+
import org.mifos.core.model.UserData
1316

1417
interface UserPreferencesRepository {
15-
suspend fun updateSettings(settings: AppSettings)
16-
suspend fun getSettings(defaultValue: AppSettings): AppSettings
18+
val userData: Flow<UserData>
19+
20+
suspend fun setThemeBrand(themeBrand: ThemeBrand)
21+
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
22+
suspend fun setDynamicColorPreference(useDynamicColor: Boolean)
23+
24+
suspend fun getThemeBrand(themeBrand: ThemeBrand)
25+
suspend fun getDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
26+
suspend fun getDynamicColorPreference(useDynamicColor: Boolean)
1727
}

core/datastore/src/commonMain/kotlin/org/mifos/core/datastore/UserPreferencesRepositoryImpl.kt

+45-14
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,59 @@
99
*/
1010
package org.mifos.core.datastore
1111

12-
import org.mifos.core.datastore.model.AppSettings
12+
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.flow.MutableStateFlow
14+
import kotlinx.coroutines.flow.asStateFlow
15+
import org.mifos.core.model.DarkThemeConfig
16+
import org.mifos.core.model.ThemeBrand
17+
import org.mifos.core.model.UserData
1318
import org.mifos.corebase.datastore.UserPreferencesDataStore
1419

15-
private const val APP_SETTINGS_KEY = "app_settings"
20+
private const val THEME_BRAND_KEY = "theme_brand"
21+
private const val DARK_THEME_CONFIG_KEY = "dark_theme_config"
22+
private const val DYNAMIC_COLOR_KEY = "use_dynamic_color"
1623

1724
class UserPreferencesRepositoryImpl(
1825
private val dataStore: UserPreferencesDataStore,
1926
) : UserPreferencesRepository {
2027

21-
override suspend fun updateSettings(settings: AppSettings) {
22-
dataStore.putValue(
23-
key = APP_SETTINGS_KEY,
24-
value = settings,
25-
serializer = AppSettings.serializer(),
26-
)
28+
private var _userData: MutableStateFlow<UserData> = MutableStateFlow(UserData())
29+
override val userData: Flow<UserData> = _userData.asStateFlow()
30+
31+
override suspend fun setThemeBrand(themeBrand: ThemeBrand) {
32+
dataStore.putValue(key = THEME_BRAND_KEY, value = themeBrand.brandName)
33+
getThemeBrand(themeBrand)
34+
}
35+
36+
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
37+
dataStore.putValue(key = DARK_THEME_CONFIG_KEY, value = darkThemeConfig.name)
38+
getDarkThemeConfig(darkThemeConfig)
39+
}
40+
41+
override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
42+
dataStore.putValue(key = DYNAMIC_COLOR_KEY, value = useDynamicColor)
43+
getDynamicColorPreference(useDynamicColor)
44+
}
45+
46+
override suspend fun getThemeBrand(themeBrand: ThemeBrand) {
47+
val themeBrandString =
48+
dataStore.getValue(key = THEME_BRAND_KEY, default = ThemeBrand.DEFAULT.brandName)
49+
_userData.value = _userData.value.copy(themeBrand = ThemeBrand.fromString(themeBrandString))
50+
}
51+
52+
override suspend fun getDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
53+
val darkThemeConfigString =
54+
dataStore.getValue(
55+
key = DARK_THEME_CONFIG_KEY,
56+
default = DarkThemeConfig.FOLLOW_SYSTEM.name,
57+
)
58+
_userData.value =
59+
_userData.value.copy(darkThemeConfig = DarkThemeConfig.fromString(darkThemeConfigString))
2760
}
2861

29-
override suspend fun getSettings(defaultValue: AppSettings): AppSettings {
30-
return dataStore.getValue(
31-
key = APP_SETTINGS_KEY,
32-
default = defaultValue,
33-
serializer = AppSettings.serializer(),
34-
)
62+
override suspend fun getDynamicColorPreference(useDynamicColor: Boolean) {
63+
val useDynamicColorBoolean =
64+
dataStore.getValue(key = DYNAMIC_COLOR_KEY, default = false)
65+
_userData.value = _userData.value.copy(useDynamicColor = useDynamicColorBoolean)
3566
}
3667
}

core/datastore/src/commonMain/kotlin/org/mifos/core/datastore/model/AppLanguage.kt

-42
This file was deleted.

core/datastore/src/commonMain/kotlin/org/mifos/core/datastore/model/SampleUser.kt

-31
This file was deleted.

0 commit comments

Comments
 (0)