Skip to content

Commit d3ccfb3

Browse files
refactor: enhance navigation and screens (#2858)
1 parent ca5bb59 commit d3ccfb3

File tree

109 files changed

+2740
-1188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+2740
-1188
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
9+
*/
10+
package cmp.android.app
11+
12+
import android.content.res.Configuration
13+
import android.graphics.Color
14+
import androidx.activity.ComponentActivity
15+
import androidx.activity.SystemBarStyle
16+
import androidx.activity.enableEdgeToEdge
17+
import androidx.annotation.ColorInt
18+
import androidx.appcompat.app.AppCompatDelegate
19+
import androidx.core.util.Consumer
20+
import androidx.lifecycle.Lifecycle
21+
import androidx.lifecycle.lifecycleScope
22+
import androidx.lifecycle.repeatOnLifecycle
23+
import kotlinx.coroutines.channels.awaitClose
24+
import kotlinx.coroutines.flow.Flow
25+
import kotlinx.coroutines.flow.callbackFlow
26+
import kotlinx.coroutines.flow.combine
27+
import kotlinx.coroutines.flow.conflate
28+
import kotlinx.coroutines.flow.distinctUntilChanged
29+
import kotlinx.coroutines.launch
30+
import org.mifos.mobile.core.model.DarkThemeConfig
31+
32+
@ColorInt
33+
private val SCRIM_COLOR: Int = Color.TRANSPARENT
34+
35+
/**
36+
* Helper method to handle edge-to-edge logic for dark mode.
37+
*
38+
* This logic is from the Now-In-Android app found
39+
* [here](https://github.com/android/nowinandroid/blob/689ef92e41427ab70f82e2c9fe59755441deae92/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt#L94).
40+
*/
41+
@Suppress("MaxLineLength")
42+
fun ComponentActivity.setupEdgeToEdge(
43+
appThemeFlow: Flow<DarkThemeConfig>,
44+
) {
45+
lifecycleScope.launch {
46+
lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
47+
combine(
48+
isSystemInDarkModeFlow(),
49+
appThemeFlow,
50+
) { isSystemDarkMode, appTheme ->
51+
AppCompatDelegate.setDefaultNightMode(appTheme.osValue)
52+
}
53+
.distinctUntilChanged()
54+
.collect { isDarkMode ->
55+
// This handles all the settings to go edge-to-edge. We are using a transparent
56+
// scrim for system bars and switching between "light" and "dark" based on the
57+
// system and internal app theme settings.
58+
val style = SystemBarStyle.auto(
59+
darkScrim = SCRIM_COLOR,
60+
lightScrim = SCRIM_COLOR,
61+
// Disabling Dark Mode for this app
62+
detectDarkMode = { false },
63+
)
64+
enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style)
65+
}
66+
}
67+
}
68+
}
69+
70+
/**
71+
* Adds a configuration change listener to retrieve whether system is in
72+
* dark theme or not. This will emit current status immediately and then
73+
* will emit changes as needed.
74+
*/
75+
private fun ComponentActivity.isSystemInDarkModeFlow(): Flow<Boolean> =
76+
callbackFlow {
77+
channel.trySend(element = resources.configuration.isSystemInDarkMode)
78+
val listener = Consumer<Configuration> {
79+
channel.trySend(element = it.isSystemInDarkMode)
80+
}
81+
addOnConfigurationChangedListener(listener = listener)
82+
awaitClose { removeOnConfigurationChangedListener(listener = listener) }
83+
}
84+
.distinctUntilChanged()
85+
.conflate()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
9+
*/
10+
package cmp.android.app
11+
12+
import android.content.res.Configuration
13+
14+
val Configuration.isSystemInDarkMode
15+
get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES

cmp-android/src/main/kotlin/cmp/android/app/MainActivity.kt

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ package cmp.android.app
1212
import android.os.Bundle
1313
import androidx.activity.ComponentActivity
1414
import androidx.activity.compose.setContent
15-
import androidx.activity.enableEdgeToEdge
15+
import androidx.appcompat.app.AppCompatDelegate
16+
import androidx.core.os.LocaleListCompat
1617
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
1718
import androidx.core.view.WindowCompat
1819
import cmp.shared.SharedApp
1920
import io.github.vinceglb.filekit.FileKit
2021
import io.github.vinceglb.filekit.dialogs.init
22+
import org.koin.android.ext.android.inject
23+
import org.mifos.mobile.core.datastore.UserPreferencesRepository
2124
import org.mifos.mobile.core.ui.utils.ShareUtils
25+
import java.util.Locale
26+
import kotlin.getValue
2227

2328
/**
2429
* Main activity class.
@@ -32,21 +37,41 @@ class MainActivity : ComponentActivity() {
3237
* Called when the activity is starting.
3338
* This is where most initialization should go: calling [setContentView(int)] to inflate the activity's UI,
3439
*/
40+
41+
private val userPreferencesRepository: UserPreferencesRepository by inject()
42+
3543
override fun onCreate(savedInstanceState: Bundle?) {
3644
super.onCreate(savedInstanceState)
45+
var shouldShowSplashScreen = true
46+
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
3747

38-
installSplashScreen()
48+
val darkThemeConfigFlow = userPreferencesRepository.observeDarkThemeConfig
3949

4050
WindowCompat.setDecorFitsSystemWindows(window, false)
41-
enableEdgeToEdge()
51+
setupEdgeToEdge(darkThemeConfigFlow)
4252
ShareUtils.setActivityProvider { return@setActivityProvider this }
4353
FileKit.init(this)
4454
/**
4555
* Set the content view of the activity.
4656
* @see setContent
4757
*/
4858
setContent {
49-
SharedApp()
59+
SharedApp(
60+
handleThemeMode = {
61+
AppCompatDelegate.setDefaultNightMode(it)
62+
},
63+
handleAppLocale = {
64+
it?.let {
65+
AppCompatDelegate.setApplicationLocales(
66+
LocaleListCompat.forLanguageTags(it),
67+
)
68+
Locale.setDefault(Locale(it))
69+
}
70+
},
71+
onSplashScreenRemoved = {
72+
shouldShowSplashScreen = false
73+
},
74+
)
5075
}
5176
}
5277
}

cmp-desktop/src/jvmMain/kotlin/main.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ fun main() {
4646
title = stringResource(Res.string.application_title),
4747
) {
4848
// Sets the content of the window.
49-
SharedApp()
49+
SharedApp(
50+
handleThemeMode = {},
51+
handleAppLocale = {},
52+
onSplashScreenRemoved = {},
53+
)
5054
}
5155
}
5256
}

cmp-navigation/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ kotlin {
4040
implementation(projects.feature.notification)
4141
implementation(projects.feature.userProfile)
4242
implementation(projects.feature.location)
43+
implementation(projects.feature.onboardingLanguage)
4344
// Core Modules
4445
implementation(projects.core.data)
4546
implementation(projects.core.common)
@@ -55,6 +56,7 @@ kotlin {
5556
implementation(libs.koin.compose)
5657
implementation(libs.koin.compose.viewmodel)
5758
implementation(libs.kotlinx.serialization.json)
59+
5860
}
5961
androidMain.dependencies {
6062
implementation(libs.androidx.core.ktx)

cmp-navigation/src/commonMain/composeResources/values/strings.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
1010
-->
1111
<resources>
12-
<string name="app_name">CMP App</string>
12+
<string name="app_name">Mifos Mobile</string>
1313
<string name="home">Home</string>
14+
<string name="transfer">Transfer</string>
1415
<string name="profile">Profile</string>
15-
<string name="settings">Settings</string>
1616
<string name="not_connected">⚠️ You aren’t connected to the internet</string>
1717
</resources>

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

Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,65 +9,41 @@
99
*/
1010
package cmp.navigation
1111

12-
import androidx.compose.foundation.isSystemInDarkTheme
13-
import androidx.compose.foundation.layout.fillMaxSize
1412
import androidx.compose.runtime.Composable
1513
import androidx.compose.runtime.getValue
1614
import androidx.compose.ui.Modifier
1715
import androidx.lifecycle.compose.collectAsStateWithLifecycle
18-
import androidx.navigation.compose.rememberNavController
19-
import cmp.navigation.navigation.NavGraphRoute.AUTH_GRAPH
20-
import cmp.navigation.navigation.NavGraphRoute.PASSCODE_GRAPH
21-
import cmp.navigation.navigation.RootNavGraph
22-
import org.koin.compose.koinInject
16+
import cmp.navigation.rootnav.RootNavScreen
2317
import org.koin.compose.viewmodel.koinViewModel
24-
import org.mifos.mobile.core.data.util.NetworkMonitor
25-
import org.mifos.mobile.core.datastore.model.AppTheme
2618
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
19+
import org.mifos.mobile.core.ui.utils.EventsEffect
2720

2821
@Composable
2922
fun ComposeApp(
23+
handleThemeMode: (osValue: Int) -> Unit,
24+
handleAppLocale: (locale: String?) -> Unit,
25+
onSplashScreenRemoved: () -> Unit,
3026
modifier: Modifier = Modifier,
31-
networkMonitor: NetworkMonitor = koinInject(),
3227
viewModel: ComposeAppViewModel = koinViewModel(),
3328
) {
34-
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
35-
val navController = rememberNavController()
29+
val uiState by viewModel.stateFlow.collectAsStateWithLifecycle()
3630

37-
val navDestination = when (uiState) {
38-
is MainUiState.Loading -> AUTH_GRAPH
39-
is MainUiState.Success -> if ((uiState as MainUiState.Success).userData.isAuthenticated) {
40-
PASSCODE_GRAPH
41-
} else {
42-
AUTH_GRAPH
31+
EventsEffect(eventFlow = viewModel.eventFlow) { event ->
32+
when (event) {
33+
is AppEvent.ShowToast -> {}
34+
is AppEvent.UpdateAppLocale -> handleAppLocale(event.localeName)
35+
is AppEvent.UpdateAppTheme -> handleThemeMode(event.osValue)
4336
}
44-
45-
else -> AUTH_GRAPH
46-
}
47-
48-
val isDarkMode = when (uiState) {
49-
is MainUiState.Success -> when ((uiState as MainUiState.Success).appTheme) {
50-
AppTheme.SYSTEM -> isSystemInDarkTheme()
51-
AppTheme.LIGHT -> false
52-
AppTheme.DARK -> true
53-
}
54-
else -> true
5537
}
5638

57-
MifosMobileTheme(isDarkMode) {
58-
RootNavGraph(
59-
modifier = modifier.fillMaxSize(),
60-
networkMonitor = networkMonitor,
61-
navHostController = navController,
62-
startDestination = navDestination,
63-
onClickLogout = {
64-
viewModel.logOut()
65-
navController.navigate(AUTH_GRAPH) {
66-
popUpTo(navController.graph.id) {
67-
inclusive = true
68-
}
69-
}
70-
},
39+
MifosMobileTheme(
40+
darkTheme = uiState.darkTheme,
41+
androidTheme = uiState.isAndroidTheme,
42+
shouldDisplayDynamicTheming = uiState.isDynamicColorsEnabled,
43+
) {
44+
RootNavScreen(
45+
modifier = modifier,
46+
onSplashScreenRemoved = onSplashScreenRemoved,
7147
)
7248
}
7349
}

0 commit comments

Comments
 (0)