@@ -22,10 +22,7 @@ import androidx.activity.SystemBarStyle
22
22
import androidx.activity.compose.setContent
23
23
import androidx.activity.enableEdgeToEdge
24
24
import androidx.activity.viewModels
25
- import androidx.compose.foundation.isSystemInDarkTheme
26
- import androidx.compose.runtime.Composable
27
25
import androidx.compose.runtime.CompositionLocalProvider
28
- import androidx.compose.runtime.DisposableEffect
29
26
import androidx.compose.runtime.getValue
30
27
import androidx.compose.runtime.mutableStateOf
31
28
import androidx.compose.runtime.setValue
@@ -35,21 +32,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
35
32
import androidx.lifecycle.lifecycleScope
36
33
import androidx.lifecycle.repeatOnLifecycle
37
34
import androidx.metrics.performance.JankStats
35
+ import androidx.tracing.trace
38
36
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
39
- import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
40
37
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
41
38
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
42
39
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
43
40
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
44
41
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
45
42
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
46
- import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
47
- import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
48
43
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
49
44
import com.google.samples.apps.nowinandroid.ui.NiaApp
50
45
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
46
+ import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme
51
47
import dagger.hilt.android.AndroidEntryPoint
52
- import kotlinx.coroutines.flow.collect
48
+ import kotlinx.coroutines.flow.combine
49
+ import kotlinx.coroutines.flow.distinctUntilChanged
50
+ import kotlinx.coroutines.flow.map
53
51
import kotlinx.coroutines.flow.onEach
54
52
import kotlinx.coroutines.launch
55
53
import javax.inject.Inject
@@ -81,53 +79,60 @@ class MainActivity : ComponentActivity() {
81
79
val splashScreen = installSplashScreen()
82
80
super .onCreate(savedInstanceState)
83
81
84
- var uiState: MainActivityUiState by mutableStateOf(Loading )
82
+ // We keep this as a mutable state, so that we can track changes inside the composition.
83
+ // This allows us to react to dark/light mode changes.
84
+ var themeSettings by mutableStateOf(
85
+ ThemeSettings (
86
+ darkTheme = resources.configuration.isSystemInDarkTheme,
87
+ androidTheme = Loading .shouldUseAndroidTheme,
88
+ disableDynamicTheming = Loading .shouldDisableDynamicTheming,
89
+ ),
90
+ )
85
91
86
92
// Update the uiState
87
93
lifecycleScope.launch {
88
94
lifecycle.repeatOnLifecycle(Lifecycle .State .STARTED ) {
89
- viewModel.uiState
90
- .onEach { uiState = it }
91
- .collect()
95
+ combine(
96
+ isSystemInDarkTheme(),
97
+ viewModel.uiState,
98
+ ) { systemDark, uiState ->
99
+ ThemeSettings (
100
+ darkTheme = uiState.shouldUseDarkTheme(systemDark),
101
+ androidTheme = uiState.shouldUseAndroidTheme,
102
+ disableDynamicTheming = uiState.shouldDisableDynamicTheming,
103
+ )
104
+ }
105
+ .onEach { themeSettings = it }
106
+ .map { it.darkTheme }
107
+ .distinctUntilChanged()
108
+ .collect { darkTheme ->
109
+ trace(" niaEdgeToEdge" ) {
110
+ // Turn off the decor fitting system windows, which allows us to handle insets,
111
+ // including IME animations, and go edge-to-edge.
112
+ // This is the same parameters as the default enableEdgeToEdge call, but we manually
113
+ // resolve whether or not to show dark theme using uiState, since it can be different
114
+ // than the configuration's dark theme value based on the user preference.
115
+ enableEdgeToEdge(
116
+ statusBarStyle = SystemBarStyle .auto(
117
+ lightScrim = android.graphics.Color .TRANSPARENT ,
118
+ darkScrim = android.graphics.Color .TRANSPARENT ,
119
+ ) { darkTheme },
120
+ navigationBarStyle = SystemBarStyle .auto(
121
+ lightScrim = lightScrim,
122
+ darkScrim = darkScrim,
123
+ ) { darkTheme },
124
+ )
125
+ }
126
+ }
92
127
}
93
128
}
94
129
95
130
// Keep the splash screen on-screen until the UI state is loaded. This condition is
96
131
// evaluated each time the app needs to be redrawn so it should be fast to avoid blocking
97
132
// the UI.
98
- splashScreen.setKeepOnScreenCondition {
99
- when (uiState) {
100
- Loading -> true
101
- is Success -> false
102
- }
103
- }
104
-
105
- // Turn off the decor fitting system windows, which allows us to handle insets,
106
- // including IME animations, and go edge-to-edge
107
- // This also sets up the initial system bar style based on the platform theme
108
- enableEdgeToEdge()
133
+ splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() }
109
134
110
135
setContent {
111
- val darkTheme = shouldUseDarkTheme(uiState)
112
-
113
- // Update the edge to edge configuration to match the theme
114
- // This is the same parameters as the default enableEdgeToEdge call, but we manually
115
- // resolve whether or not to show dark theme using uiState, since it can be different
116
- // than the configuration's dark theme value based on the user preference.
117
- DisposableEffect (darkTheme) {
118
- enableEdgeToEdge(
119
- statusBarStyle = SystemBarStyle .auto(
120
- android.graphics.Color .TRANSPARENT ,
121
- android.graphics.Color .TRANSPARENT ,
122
- ) { darkTheme },
123
- navigationBarStyle = SystemBarStyle .auto(
124
- lightScrim,
125
- darkScrim,
126
- ) { darkTheme },
127
- )
128
- onDispose {}
129
- }
130
-
131
136
val appState = rememberNiaAppState(
132
137
networkMonitor = networkMonitor,
133
138
userNewsResourceRepository = userNewsResourceRepository,
@@ -141,9 +146,9 @@ class MainActivity : ComponentActivity() {
141
146
LocalTimeZone provides currentTimeZone,
142
147
) {
143
148
NiaTheme (
144
- darkTheme = darkTheme,
145
- androidTheme = shouldUseAndroidTheme(uiState) ,
146
- disableDynamicTheming = shouldDisableDynamicTheming(uiState) ,
149
+ darkTheme = themeSettings. darkTheme,
150
+ androidTheme = themeSettings.androidTheme ,
151
+ disableDynamicTheming = themeSettings.disableDynamicTheming ,
147
152
) {
148
153
NiaApp (appState)
149
154
}
@@ -162,47 +167,6 @@ class MainActivity : ComponentActivity() {
162
167
}
163
168
}
164
169
165
- /* *
166
- * Returns `true` if the Android theme should be used, as a function of the [uiState].
167
- */
168
- @Composable
169
- private fun shouldUseAndroidTheme (
170
- uiState : MainActivityUiState ,
171
- ): Boolean = when (uiState) {
172
- Loading -> false
173
- is Success -> when (uiState.userData.themeBrand) {
174
- ThemeBrand .DEFAULT -> false
175
- ThemeBrand .ANDROID -> true
176
- }
177
- }
178
-
179
- /* *
180
- * Returns `true` if the dynamic color is disabled, as a function of the [uiState].
181
- */
182
- @Composable
183
- private fun shouldDisableDynamicTheming (
184
- uiState : MainActivityUiState ,
185
- ): Boolean = when (uiState) {
186
- Loading -> false
187
- is Success -> ! uiState.userData.useDynamicColor
188
- }
189
-
190
- /* *
191
- * Returns `true` if dark theme should be used, as a function of the [uiState] and the
192
- * current system context.
193
- */
194
- @Composable
195
- private fun shouldUseDarkTheme (
196
- uiState : MainActivityUiState ,
197
- ): Boolean = when (uiState) {
198
- Loading -> isSystemInDarkTheme()
199
- is Success -> when (uiState.userData.darkThemeConfig) {
200
- DarkThemeConfig .FOLLOW_SYSTEM -> isSystemInDarkTheme()
201
- DarkThemeConfig .LIGHT -> false
202
- DarkThemeConfig .DARK -> true
203
- }
204
- }
205
-
206
170
/* *
207
171
* The default light scrim, as defined by androidx and the platform:
208
172
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
@@ -214,3 +178,13 @@ private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
214
178
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
215
179
*/
216
180
private val darkScrim = android.graphics.Color .argb(0x80 , 0x1b , 0x1b , 0x1b )
181
+
182
+ /* *
183
+ * Class for the system theme settings.
184
+ * This wrapping class allows us to combine all the changes and prevent unnecessary recompositions.
185
+ */
186
+ data class ThemeSettings (
187
+ val darkTheme : Boolean ,
188
+ val androidTheme : Boolean ,
189
+ val disableDynamicTheming : Boolean ,
190
+ )
0 commit comments