Add data logic for theme switcher

Change-Id: Ifffadb897de4f6e08f7115103f99c156a7098b70
pull/330/head
Alex Vanyo 2 years ago committed by Don Turner
parent a0b22d8ed2
commit 7e3faad699

@ -83,8 +83,11 @@ dependencies {
implementation(project(":feature:bookmarks"))
implementation(project(":feature:topic"))
implementation(project(":core:common"))
implementation(project(":core:ui"))
implementation(project(":core:designsystem"))
implementation(project(":core:data"))
implementation(project(":core:model"))
implementation(project(":sync:work"))
implementation(project(":sync:sync-test"))
@ -96,6 +99,7 @@ dependencies {
androidTestImplementation(libs.androidx.navigation.testing)
debugImplementation(libs.androidx.compose.ui.testManifest)
implementation(libs.accompanist.systemuicontroller)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)

@ -19,14 +19,34 @@ package com.google.samples.apps.nowinandroid
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.metrics.performance.JankStats
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
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.model.data.UserData
import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult
import com.google.samples.apps.nowinandroid.ui.NiaApp
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint
@ -38,16 +58,60 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var lazyStats: dagger.Lazy<JankStats>
@Inject
lateinit var userDataRepository: UserDataRepository
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
/**
* The current user data, updated here to drive the UI theme
*/
var userDataResult: Result<UserData> by mutableStateOf(Result.Loading)
// Update the user data
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
userDataRepository.userDataStream
.asResult()
.onEach {
userDataResult = it
}
.collect()
}
}
// Keep the splash screen on-screen until the user data is loaded
splashScreen.setKeepOnScreenCondition {
when (userDataResult) {
Result.Loading -> true
is Result.Success, is Result.Error -> false
}
}
// Turn off the decor fitting system windows, which allows us to handle insets,
// including IME animations
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
NiaApp(calculateWindowSizeClass(this))
val systemUiController = rememberSystemUiController()
val darkTheme = shouldUseDarkTheme(userDataResult)
// Update the dark content of the system bars to match the theme
DisposableEffect(systemUiController, darkTheme) {
systemUiController.systemBarsDarkContentEnabled = !darkTheme
onDispose {}
}
NiaTheme(
darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(userDataResult)
) {
NiaApp(
windowSizeClass = calculateWindowSizeClass(this),
)
}
}
}
@ -61,3 +125,33 @@ class MainActivity : ComponentActivity() {
lazyStats.get().isTrackingEnabled = false
}
}
/**
* Returns `true` if the Android theme shoudl be used, as a function of the [userDataResult].
*/
@Composable
fun shouldUseAndroidTheme(
userDataResult: Result<UserData>,
): Boolean = when (userDataResult) {
Result.Loading, is Result.Error -> false
is Result.Success -> when (userDataResult.data.themeBrand) {
ThemeBrand.DEFAULT -> false
ThemeBrand.ANDROID -> true
}
}
/**
* Returns `true` if dark theme should be used, as a function of the [userDataResult] and the
* current system context.
*/
@Composable
fun shouldUseDarkTheme(
userDataResult: Result<UserData>,
): Boolean = when (userDataResult) {
Result.Loading, is Result.Error -> isSystemInDarkTheme()
is Result.Success -> when (userDataResult.data.darkThemeConfig) {
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
DarkThemeConfig.LIGHT -> false
DarkThemeConfig.DARK -> true
}
}

@ -65,59 +65,56 @@ fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme {
val background: @Composable (@Composable () -> Unit) -> Unit =
when (appState.currentDestination?.route) {
TopLevelDestination.FOR_YOU.name -> { content ->
NiaGradientBackground(content = content)
}
else -> { content -> NiaBackground(content = content) }
}
val background: @Composable (@Composable () -> Unit) -> Unit =
when (appState.currentDestination?.route) {
TopLevelDestination.FOR_YOU.name -> { content -> NiaGradientBackground(content = content) }
else -> { content -> NiaBackground(content = content) }
}
background {
Scaffold(
modifier = Modifier.semantics {
testTagsAsResourceId = true
},
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
bottomBar = {
if (appState.shouldShowBottomBar) {
NiaBottomBar(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination
)
}
background {
Scaffold(
modifier = Modifier.semantics {
testTagsAsResourceId = true
},
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
bottomBar = {
if (appState.shouldShowBottomBar) {
NiaBottomBar(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination
)
}
) { padding ->
Row(
Modifier
.fillMaxSize()
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
)
)
) {
if (appState.shouldShowNavRail) {
NiaNavRail(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination,
modifier = Modifier.safeDrawingPadding()
}
) { padding ->
Row(
Modifier
.fillMaxSize()
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
)
}
NiaNavHost(
navController = appState.navController,
onBackClick = appState::onBackClick,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding)
)
) {
if (appState.shouldShowNavRail) {
NiaNavRail(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination,
modifier = Modifier.safeDrawingPadding()
)
}
NiaNavHost(
navController = appState.navController,
onBackClick = appState::onBackClick,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding)
)
}
}
}
@ -182,6 +179,7 @@ private fun NiaBottomBar(
imageVector = icon.imageVector,
contentDescription = null
)
is DrawableResourceIcon -> Icon(
painter = painterResource(id = icon.id),
contentDescription = null

@ -17,6 +17,8 @@
package com.google.samples.apps.nowinandroid.core.data.repository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
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.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@ -42,4 +44,10 @@ class OfflineFirstUserDataRepository @Inject constructor(
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) =
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
override suspend fun setThemeBrand(themeBrand: ThemeBrand) =
niaPreferencesDataSource.setThemeBrand(themeBrand)
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) =
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
}

@ -16,6 +16,8 @@
package com.google.samples.apps.nowinandroid.core.data.repository
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.model.data.UserData
import kotlinx.coroutines.flow.Flow
@ -50,4 +52,14 @@ interface UserDataRepository {
* Updates the bookmarked status for a news resource
*/
suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
/**
* Sets the desired theme brand.
*/
suspend fun setThemeBrand(themeBrand: ThemeBrand)
/**
* Sets the desired dark theme config.
*/
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
}

@ -19,6 +19,8 @@ package com.google.samples.apps.nowinandroid.core.data.repository.fake
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
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.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@ -53,4 +55,12 @@ class FakeUserDataRepository @Inject constructor(
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
}
override suspend fun setThemeBrand(themeBrand: ThemeBrand) {
niaPreferencesDataSource.setThemeBrand(themeBrand)
}
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
}
}

@ -18,6 +18,9 @@ package com.google.samples.apps.nowinandroid.core.data.repository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
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.model.data.UserData
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runTest
@ -46,6 +49,21 @@ class OfflineFirstUserDataRepositoryTest {
)
}
@Test
fun offlineFirstUserDataRepository_default_user_data_is_correct() =
runTest {
assertEquals(
UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = emptySet(),
followedAuthors = emptySet(),
themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM
),
subject.userDataStream.first()
)
}
@Test
fun offlineFirstUserDataRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() =
runTest {
@ -129,4 +147,44 @@ class OfflineFirstUserDataRepositoryTest {
.first()
)
}
@Test
fun offlineFirstUserDataRepository_set_theme_brand_delegates_to_nia_preferences() =
runTest {
subject.setThemeBrand(ThemeBrand.ANDROID)
assertEquals(
ThemeBrand.ANDROID,
subject.userDataStream
.map { it.themeBrand }
.first()
)
assertEquals(
ThemeBrand.ANDROID,
niaPreferencesDataSource
.userDataStream
.map { it.themeBrand }
.first()
)
}
@Test
fun offlineFirstUserDataRepository_set_dark_theme_config_delegates_to_nia_preferences() =
runTest {
subject.setDarkThemeConfig(DarkThemeConfig.DARK)
assertEquals(
DarkThemeConfig.DARK,
subject.userDataStream
.map { it.darkThemeConfig }
.first()
)
assertEquals(
DarkThemeConfig.DARK,
niaPreferencesDataSource
.userDataStream
.map { it.darkThemeConfig }
.first()
)
}
}

@ -18,6 +18,10 @@ package com.google.samples.apps.nowinandroid.core.datastore
import android.util.Log
import androidx.datastore.core.DataStore
import com.google.protobuf.kotlin.DslList
import com.google.protobuf.kotlin.DslProxy
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.model.data.UserData
import java.io.IOException
import javax.inject.Inject
@ -34,6 +38,21 @@ class NiaPreferencesDataSource @Inject constructor(
bookmarkedNewsResources = it.bookmarkedNewsResourceIdsMap.keys,
followedTopics = it.followedTopicIdsMap.keys,
followedAuthors = it.followedAuthorIdsMap.keys,
themeBrand = when (it.themeBrand!!) {
ThemeBrandProto.THEME_BRAND_UNSPECIFIED,
ThemeBrandProto.UNRECOGNIZED,
ThemeBrandProto.THEME_BRAND_DEFAULT -> ThemeBrand.DEFAULT
ThemeBrandProto.THEME_BRAND_ANDROID -> ThemeBrand.ANDROID
},
darkThemeConfig = when (it.darkThemeConfig!!) {
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
DarkThemeConfigProto.UNRECOGNIZED,
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM ->
DarkThemeConfig.FOLLOW_SYSTEM
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT ->
DarkThemeConfig.LIGHT
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK
}
)
}
@ -95,6 +114,30 @@ class NiaPreferencesDataSource @Inject constructor(
}
}
suspend fun setThemeBrand(themeBrand: ThemeBrand) {
userPreferences.updateData {
it.copy {
this.themeBrand = when (themeBrand) {
ThemeBrand.DEFAULT -> ThemeBrandProto.THEME_BRAND_DEFAULT
ThemeBrand.ANDROID -> ThemeBrandProto.THEME_BRAND_ANDROID
}
}
}
}
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
userPreferences.updateData {
it.copy {
this.darkThemeConfig = when (darkThemeConfig) {
DarkThemeConfig.FOLLOW_SYSTEM ->
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
DarkThemeConfig.LIGHT -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
DarkThemeConfig.DARK -> DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
}
}
}
}
suspend fun toggleNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
try {
userPreferences.updateData {

@ -0,0 +1,27 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
syntax = "proto3";
option java_package = "com.google.samples.apps.nowinandroid.core.datastore";
option java_multiple_files = true;
enum DarkThemeConfigProto {
DARK_THEME_CONFIG_UNSPECIFIED = 0;
DARK_THEME_CONFIG_FOLLOW_SYSTEM = 1;
DARK_THEME_CONFIG_LIGHT = 2;
DARK_THEME_CONFIG_DARK = 3;
}

@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
syntax = "proto3";
option java_package = "com.google.samples.apps.nowinandroid.core.datastore";
option java_multiple_files = true;
enum ThemeBrandProto {
THEME_BRAND_UNSPECIFIED = 0;
THEME_BRAND_DEFAULT = 1;
THEME_BRAND_ANDROID = 2;
}

@ -16,6 +16,9 @@
syntax = "proto3";
import "com/google/samples/apps/nowinandroid/data/dark_theme_config.proto";
import "com/google/samples/apps/nowinandroid/data/theme_brand.proto";
option java_package = "com.google.samples.apps.nowinandroid.core.datastore";
option java_multiple_files = true;
@ -37,4 +40,6 @@ message UserPreferences {
map<string, bool> followed_topic_ids = 13;
map<string, bool> followed_author_ids = 14;
map<string, bool> bookmarked_news_resource_ids = 15;
ThemeBrandProto theme_brand = 12;
DarkThemeConfigProto dark_theme_config = 13;
}

@ -58,7 +58,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = false,
disableDynamicTheming = true,
androidTheme = false
) {
val colorScheme = LightDefaultColorScheme
@ -79,7 +79,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = false,
disableDynamicTheming = true,
androidTheme = false
) {
val colorScheme = DarkDefaultColorScheme
@ -100,7 +100,6 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = true,
androidTheme = false
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -129,7 +128,6 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = true,
androidTheme = false
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -154,7 +152,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = false,
disableDynamicTheming = true,
androidTheme = true
) {
val colorScheme = LightAndroidColorScheme
@ -172,7 +170,7 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = false,
disableDynamicTheming = true,
androidTheme = true
) {
val colorScheme = DarkAndroidColorScheme
@ -190,25 +188,13 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = false,
dynamicColor = true,
androidTheme = true
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicLightColorScheme(LocalContext.current)
} else {
LightDefaultColorScheme
}
val colorScheme = LightAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
GradientColors()
} else {
LightDefaultGradientColors
}
val gradientColors = GradientColors()
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
val backgroundTheme = LightAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}
@ -219,21 +205,13 @@ class ThemeTest {
composeTestRule.setContent {
NiaTheme(
darkTheme = true,
dynamicColor = true,
androidTheme = true
) {
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicDarkColorScheme(LocalContext.current)
} else {
DarkDefaultColorScheme
}
val colorScheme = DarkAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
val gradientColors = GradientColors()
assertEquals(gradientColors, LocalGradientColors.current)
val backgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
val backgroundTheme = DarkAndroidBackgroundTheme
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
}
}

@ -144,7 +144,7 @@ annotation class ThemePreviews
@ThemePreviews
@Composable
fun BackgroundDefault() {
NiaTheme {
NiaTheme(disableDynamicTheming = true) {
NiaBackground(Modifier.size(100.dp), content = {})
}
}
@ -152,7 +152,7 @@ fun BackgroundDefault() {
@ThemePreviews
@Composable
fun BackgroundDynamic() {
NiaTheme(dynamicColor = true) {
NiaTheme {
NiaBackground(Modifier.size(100.dp), content = {})
}
}
@ -168,7 +168,7 @@ fun BackgroundAndroid() {
@ThemePreviews
@Composable
fun GradientBackgroundDefault() {
NiaTheme {
NiaTheme(disableDynamicTheming = true) {
NiaGradientBackground(Modifier.size(100.dp), content = {})
}
}
@ -176,7 +176,7 @@ fun GradientBackgroundDefault() {
@ThemePreviews
@Composable
fun GradientBackgroundDynamic() {
NiaTheme(dynamicColor = true) {
NiaTheme {
NiaGradientBackground(Modifier.size(100.dp), content = {})
}
}

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.core.designsystem.theme
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@ -173,55 +174,63 @@ val DarkAndroidBackgroundTheme = BackgroundTheme(color = Color.Black)
/**
* Now in Android theme.
*
* The order of precedence for the color scheme is: Dynamic color > Android theme > Default theme.
* Dark theme is independent as all the aforementioned color schemes have light and dark versions.
* The default theme color scheme is used by default.
*
* @param darkTheme Whether the theme should use a dark color scheme (follows system by default).
* @param dynamicColor Whether the theme should use a dynamic color scheme (Android 12+ only).
* @param androidTheme Whether the theme should use the Android theme color scheme.
* @param androidTheme Whether the theme should use the Android theme color scheme instead of the
* default theme. If this is `false`, then dynamic theming will be used when supported.
*/
@Composable
fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
androidTheme: Boolean = false,
content: @Composable() () -> Unit
content: @Composable () -> Unit
) = NiaTheme(
darkTheme = darkTheme,
androidTheme = androidTheme,
disableDynamicTheming = false,
content = content
)
/**
* Now in Android theme. This is an internal only version, to allow disabling dynamic theming
* in tests.
*
* @param darkTheme Whether the theme should use a dark color scheme (follows system by default).
* @param androidTheme Whether the theme should use the Android theme color scheme instead of the
* default theme.
* @param disableDynamicTheming If `true`, disables the use of dynamic theming, even when it is
* supported. This parameter has no effect if [androidTheme] is `true`.
*/
@Composable
internal fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
androidTheme: Boolean = false,
disableDynamicTheming: Boolean,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} else {
if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
}
}
androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
val colorScheme = if (androidTheme) {
if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme
} else if (!disableDynamicTheming && supportsDynamicTheming()) {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} else {
if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme
}
val defaultGradientColors = GradientColors()
val gradientColors = when {
dynamicColor -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
defaultGradientColors
} else {
if (darkTheme) defaultGradientColors else LightDefaultGradientColors
}
}
androidTheme -> defaultGradientColors
else -> if (darkTheme) defaultGradientColors else LightDefaultGradientColors
val gradientColors = if (androidTheme || (!disableDynamicTheming && supportsDynamicTheming())) {
defaultGradientColors
} else {
if (darkTheme) defaultGradientColors else LightDefaultGradientColors
}
val defaultBackgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
)
val backgroundTheme = when {
dynamicColor -> defaultBackgroundTheme
androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
else -> defaultBackgroundTheme
val backgroundTheme = if (androidTheme) {
if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
} else {
defaultBackgroundTheme
}
CompositionLocalProvider(
@ -235,3 +244,6 @@ fun NiaTheme(
)
}
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
private fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

@ -0,0 +1,21 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.data
enum class DarkThemeConfig {
FOLLOW_SYSTEM, LIGHT, DARK
}

@ -0,0 +1,21 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.data
enum class ThemeBrand {
DEFAULT, ANDROID
}

@ -23,4 +23,6 @@ data class UserData(
val bookmarkedNewsResources: Set<String>,
val followedTopics: Set<String>,
val followedAuthors: Set<String>,
val themeBrand: ThemeBrand,
val darkThemeConfig: DarkThemeConfig,
)

@ -17,6 +17,8 @@
package com.google.samples.apps.nowinandroid.core.testing.repository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
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.model.data.UserData
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.flow.Flow
@ -26,7 +28,9 @@ import kotlinx.coroutines.flow.filterNotNull
private val emptyUserData = UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = emptySet(),
followedAuthors = emptySet()
followedAuthors = emptySet(),
themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM
)
class TestUserDataRepository : UserDataRepository {
@ -74,6 +78,18 @@ class TestUserDataRepository : UserDataRepository {
}
}
override suspend fun setThemeBrand(themeBrand: ThemeBrand) {
currentUserData.let { current ->
_userData.tryEmit(current.copy(themeBrand = themeBrand))
}
}
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
currentUserData.let { current ->
_userData.tryEmit(current.copy(darkThemeConfig = darkThemeConfig))
}
}
/**
* A test-only API to allow setting/unsetting of bookmarks.
*

@ -52,6 +52,7 @@ turbine = "0.8.0"
[libraries]
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" }
accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" }
android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }

Loading…
Cancel
Save