@ -22,12 +22,9 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.setValue
@ -38,27 +35,22 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.metrics.performance.JankStats
import androidx.metrics.performance.JankStats
import androidx.tracing.trace
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
import com.google.samples.apps.nowinandroid.ui.NiaApp
import com.google.samples.apps.nowinandroid.ui.NiaApp
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.co llect
import kotlinx.coroutines.flow.co mbine
import kotlinx.coroutines.flow. onEach
import kotlinx.coroutines.flow. distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Inject
private const val TAG = " MainActivity "
@OptIn ( ExperimentalMaterial3WindowSizeClassApi :: class )
@OptIn ( ExperimentalMaterial3WindowSizeClassApi :: class )
@AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity ( ) {
class MainActivity : ComponentActivity ( ) {
@ -82,60 +74,64 @@ class MainActivity : ComponentActivity() {
lateinit var userNewsResourceRepository : UserNewsResourceRepository
lateinit var userNewsResourceRepository : UserNewsResourceRepository
val viewModel : MainActivityViewModel by viewModels ( )
val viewModel : MainActivityViewModel by viewModels ( )
override fun onCreate ( savedInstanceState : Bundle ? ) {
override fun onCreate ( savedInstanceState : Bundle ? ) {
val splashScreen = installSplashScreen ( )
val splashScreen = installSplashScreen ( )
super . onCreate ( savedInstanceState )
super . onCreate ( savedInstanceState )
var uiState : MainActivityUiState by mutableStateOf ( Loading )
// We keep this as a mutable state, so that we can track changes inside the composition.
// This allows us to react to dark/light mode changes.
var themeSettings by mutableStateOf (
ThemeSettings (
darkTheme = resources . configuration . isSystemInDarkTheme ,
androidTheme = false ,
disableDynamicTheming = true ,
) ,
)
// Update the uiState
// Update the uiState
lifecycleScope . launch {
lifecycleScope . launch {
lifecycle . repeatOnLifecycle ( Lifecycle . State . STARTED ) {
lifecycle . repeatOnLifecycle ( Lifecycle . State . STARTED ) {
viewModel . uiState
combine (
. onEach { uiState = it }
isSystemInDarkTheme ( ) ,
. collect ( )
viewModel . uiState ,
}
) { systemDark , uiState ->
}
ThemeSettings (
darkTheme = uiState . shouldUseDarkTheme ( systemDark ) ,
// Keep the splash screen on-screen until the UI state is loaded. This condition is
androidTheme = uiState . shouldUseAndroidTheme ,
// evaluated each time the app needs to be redrawn so it should be fast to avoid blocking
disableDynamicTheming = uiState . shouldDisableDynamicTheming ,
// the UI.
)
splashScreen . setKeepOnScreenCondition {
when ( uiState ) {
Loading -> true
is Success -> false
}
}
}
. distinctUntilChanged ( )
. collect { newThemeSettings ->
trace ( " niaEdgeToEdge " ) {
// Turn off the decor fitting system windows, which allows us to handle insets,
// Turn off the decor fitting system windows, which allows us to handle insets,
// including IME animations, and go edge-to-edge
// including IME animations, and go edge-to-edge.
// This also sets up the initial system bar style based on the platform theme
trace ( " niaEdgeToEdge " ) { enableEdgeToEdge ( ) }
setContent {
val darkTheme = shouldUseDarkTheme ( uiState )
// Update the edge to edge configuration to match the theme
// This is the same parameters as the default enableEdgeToEdge call, but we manually
// This is the same parameters as the default enableEdgeToEdge call, but we manually
// resolve whether or not to show dark theme using uiState, since it can be different
// resolve whether or not to show dark theme using uiState, since it can be different
// than the configuration's dark theme value based on the user preference.
// than the configuration's dark theme value based on the user preference.
DisposableEffect ( darkTheme ) {
trace ( " niaEdgeToEdge " ) {
enableEdgeToEdge (
enableEdgeToEdge (
statusBarStyle = SystemBarStyle . auto (
statusBarStyle = SystemBarStyle . auto (
android . graphics . Color . TRANSPARENT ,
lightScrim = android . graphics . Color . TRANSPARENT ,
android . graphics . Color . TRANSPARENT ,
darkScrim = android . graphics . Color . TRANSPARENT ,
) { darkTheme } ,
) { newThemeSettings . darkTheme } ,
navigationBarStyle = SystemBarStyle . auto (
navigationBarStyle = SystemBarStyle . auto (
lightScrim ,
lightScrim = lightScrim ,
darkScrim ,
darkScrim = darkScrim ,
) { darkTheme } ,
) { newThemeSettings . darkTheme } ,
)
)
onDispose { }
}
themeSettings = newThemeSettings
}
}
}
}
}
// Keep the splash screen on-screen until the UI state is loaded. This condition is
// evaluated each time the app needs to be redrawn so it should be fast to avoid blocking
// the UI.
splashScreen . setKeepOnScreenCondition { viewModel . uiState . value . shouldKeepSplashScreen ( ) }
setContent {
val appState = rememberNiaAppState (
val appState = rememberNiaAppState (
windowSizeClass = calculateWindowSizeClass ( this ) ,
windowSizeClass = calculateWindowSizeClass ( this ) ,
networkMonitor = networkMonitor ,
networkMonitor = networkMonitor ,
@ -150,9 +146,9 @@ class MainActivity : ComponentActivity() {
LocalTimeZone provides currentTimeZone ,
LocalTimeZone provides currentTimeZone ,
) {
) {
NiaTheme (
NiaTheme (
darkTheme = darkTheme,
darkTheme = themeSettings. darkTheme,
androidTheme = shouldUseAndroidTheme( uiState ) ,
androidTheme = themeSettings. androidTheme ,
disableDynamicTheming = shouldDisableDynamicTheming( uiState ) ,
disableDynamicTheming = themeSettings. disableDynamicTheming ,
) {
) {
NiaApp ( appState )
NiaApp ( appState )
}
}
@ -171,47 +167,6 @@ class MainActivity : ComponentActivity() {
}
}
}
}
/ * *
* Returns `true` if the Android theme should be used , as a function of the [ uiState ] .
* /
@Composable
private fun shouldUseAndroidTheme (
uiState : MainActivityUiState ,
) : Boolean = when ( uiState ) {
Loading -> false
is Success -> when ( uiState . userData . themeBrand ) {
ThemeBrand . DEFAULT -> false
ThemeBrand . ANDROID -> true
}
}
/ * *
* Returns `true` if the dynamic color is disabled , as a function of the [ uiState ] .
* /
@Composable
private fun shouldDisableDynamicTheming (
uiState : MainActivityUiState ,
) : Boolean = when ( uiState ) {
Loading -> false
is Success -> ! uiState . userData . useDynamicColor
}
/ * *
* Returns `true` if dark theme should be used , as a function of the [ uiState ] and the
* current system context .
* /
@Composable
private fun shouldUseDarkTheme (
uiState : MainActivityUiState ,
) : Boolean = when ( uiState ) {
Loading -> isSystemInDarkTheme ( )
is Success -> when ( uiState . userData . darkThemeConfig ) {
DarkThemeConfig . FOLLOW _SYSTEM -> isSystemInDarkTheme ( )
DarkThemeConfig . LIGHT -> false
DarkThemeConfig . DARK -> true
}
}
/ * *
/ * *
* The default light scrim , as defined by androidx and the platform :
* The default light scrim , as defined by androidx and the platform :
* https : //cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
* https : //cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
@ -223,3 +178,13 @@ private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
* https : //cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
* https : //cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
* /
* /
private val darkScrim = android . graphics . Color . argb ( 0x80 , 0x1b , 0x1b , 0x1b )
private val darkScrim = android . graphics . Color . argb ( 0x80 , 0x1b , 0x1b , 0x1b )
/ * *
* Class for the system theme settings .
* This wrapping class allows us to combine all the changes and prevent unnecessary recompositions .
* /
data class ThemeSettings (
val darkTheme : Boolean ,
val androidTheme : Boolean ,
val disableDynamicTheming : Boolean ,
)