Merge pull request #543 from android/mb/trailing-commas

Enable trailing commas via .editorconfig
pull/547/head
Márton Braun 2 years ago committed by GitHub
commit 11fbf53f12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,6 @@
# https://editorconfig.org/
# This configuration is used by ktlint when spotless invokes it
[*.{kt,kts}]
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true

@ -66,7 +66,7 @@ fun NiaCatalog() {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = contentPadding, contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
item { item {
Text( Text(
@ -93,19 +93,19 @@ fun NiaCatalog() {
FlowRow(mainAxisSpacing = 16.dp) { FlowRow(mainAxisSpacing = 16.dp) {
NiaButton( NiaButton(
onClick = {}, onClick = {},
enabled = false enabled = false,
) { ) {
Text(text = "Disabled") Text(text = "Disabled")
} }
NiaOutlinedButton( NiaOutlinedButton(
onClick = {}, onClick = {},
enabled = false enabled = false,
) { ) {
Text(text = "Disabled") Text(text = "Disabled")
} }
NiaTextButton( NiaTextButton(
onClick = {}, onClick = {},
enabled = false enabled = false,
) { ) {
Text(text = "Disabled") Text(text = "Disabled")
} }
@ -119,21 +119,21 @@ fun NiaCatalog() {
text = { Text(text = "Enabled") }, text = { Text(text = "Enabled") },
leadingIcon = { leadingIcon = {
Icon(imageVector = NiaIcons.Add, contentDescription = null) Icon(imageVector = NiaIcons.Add, contentDescription = null)
} },
) )
NiaOutlinedButton( NiaOutlinedButton(
onClick = {}, onClick = {},
text = { Text(text = "Enabled") }, text = { Text(text = "Enabled") },
leadingIcon = { leadingIcon = {
Icon(imageVector = NiaIcons.Add, contentDescription = null) Icon(imageVector = NiaIcons.Add, contentDescription = null)
} },
) )
NiaTextButton( NiaTextButton(
onClick = {}, onClick = {},
text = { Text(text = "Enabled") }, text = { Text(text = "Enabled") },
leadingIcon = { leadingIcon = {
Icon(imageVector = NiaIcons.Add, contentDescription = null) Icon(imageVector = NiaIcons.Add, contentDescription = null)
} },
) )
} }
} }
@ -146,7 +146,7 @@ fun NiaCatalog() {
text = { Text(text = "Disabled") }, text = { Text(text = "Disabled") },
leadingIcon = { leadingIcon = {
Icon(imageVector = NiaIcons.Add, contentDescription = null) Icon(imageVector = NiaIcons.Add, contentDescription = null)
} },
) )
NiaOutlinedButton( NiaOutlinedButton(
onClick = {}, onClick = {},
@ -154,7 +154,7 @@ fun NiaCatalog() {
text = { Text(text = "Disabled") }, text = { Text(text = "Disabled") },
leadingIcon = { leadingIcon = {
Icon(imageVector = NiaIcons.Add, contentDescription = null) Icon(imageVector = NiaIcons.Add, contentDescription = null)
} },
) )
NiaTextButton( NiaTextButton(
onClick = {}, onClick = {},
@ -162,7 +162,7 @@ fun NiaCatalog() {
text = { Text(text = "Disabled") }, text = { Text(text = "Disabled") },
leadingIcon = { leadingIcon = {
Icon(imageVector = NiaIcons.Add, contentDescription = null) Icon(imageVector = NiaIcons.Add, contentDescription = null)
} },
) )
} }
} }
@ -173,14 +173,14 @@ fun NiaCatalog() {
text = { Text("Enabled") }, text = { Text("Enabled") },
items = listOf("Item 1", "Item 2", "Item 3"), items = listOf("Item 1", "Item 2", "Item 3"),
onItemClick = {}, onItemClick = {},
itemText = { item -> Text(item) } itemText = { item -> Text(item) },
) )
NiaDropdownMenuButton( NiaDropdownMenuButton(
text = { Text("Disabled") }, text = { Text("Disabled") },
items = listOf("Item 1", "Item 2", "Item 3"), items = listOf("Item 1", "Item 2", "Item 3"),
onItemClick = {}, onItemClick = {},
itemText = { item -> Text(item) }, itemText = { item -> Text(item) },
enabled = false enabled = false,
) )
} }
} }
@ -191,25 +191,25 @@ fun NiaCatalog() {
NiaFilterChip( NiaFilterChip(
selected = firstChecked, selected = firstChecked,
onSelectedChange = { checked -> firstChecked = checked }, onSelectedChange = { checked -> firstChecked = checked },
label = { Text(text = "Enabled") } label = { Text(text = "Enabled") },
) )
var secondChecked by remember { mutableStateOf(true) } var secondChecked by remember { mutableStateOf(true) }
NiaFilterChip( NiaFilterChip(
selected = secondChecked, selected = secondChecked,
onSelectedChange = { checked -> secondChecked = checked }, onSelectedChange = { checked -> secondChecked = checked },
label = { Text(text = "Enabled") } label = { Text(text = "Enabled") },
) )
NiaFilterChip( NiaFilterChip(
selected = false, selected = false,
onSelectedChange = {}, onSelectedChange = {},
enabled = false, enabled = false,
label = { Text(text = "Disabled") } label = { Text(text = "Disabled") },
) )
NiaFilterChip( NiaFilterChip(
selected = true, selected = true,
onSelectedChange = {}, onSelectedChange = {},
enabled = false, enabled = false,
label = { Text(text = "Disabled") } label = { Text(text = "Disabled") },
) )
} }
} }
@ -223,15 +223,15 @@ fun NiaCatalog() {
icon = { icon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.BookmarkBorder), painter = painterResource(id = NiaIcons.BookmarkBorder),
contentDescription = null contentDescription = null,
) )
}, },
checkedIcon = { checkedIcon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.Bookmark), painter = painterResource(id = NiaIcons.Bookmark),
contentDescription = null contentDescription = null,
) )
} },
) )
var secondChecked by remember { mutableStateOf(true) } var secondChecked by remember { mutableStateOf(true) }
NiaIconToggleButton( NiaIconToggleButton(
@ -240,15 +240,15 @@ fun NiaCatalog() {
icon = { icon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.BookmarkBorder), painter = painterResource(id = NiaIcons.BookmarkBorder),
contentDescription = null contentDescription = null,
) )
}, },
checkedIcon = { checkedIcon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.Bookmark), painter = painterResource(id = NiaIcons.Bookmark),
contentDescription = null contentDescription = null,
) )
} },
) )
NiaIconToggleButton( NiaIconToggleButton(
checked = false, checked = false,
@ -256,16 +256,16 @@ fun NiaCatalog() {
icon = { icon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.BookmarkBorder), painter = painterResource(id = NiaIcons.BookmarkBorder),
contentDescription = null contentDescription = null,
) )
}, },
checkedIcon = { checkedIcon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.Bookmark), painter = painterResource(id = NiaIcons.Bookmark),
contentDescription = null contentDescription = null,
) )
}, },
enabled = false enabled = false,
) )
NiaIconToggleButton( NiaIconToggleButton(
checked = true, checked = true,
@ -273,16 +273,16 @@ fun NiaCatalog() {
icon = { icon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.BookmarkBorder), painter = painterResource(id = NiaIcons.BookmarkBorder),
contentDescription = null contentDescription = null,
) )
}, },
checkedIcon = { checkedIcon = {
Icon( Icon(
painter = painterResource(id = NiaIcons.Bookmark), painter = painterResource(id = NiaIcons.Bookmark),
contentDescription = null contentDescription = null,
) )
}, },
enabled = false enabled = false,
) )
} }
} }
@ -294,21 +294,21 @@ fun NiaCatalog() {
expanded = firstExpanded, expanded = firstExpanded,
onExpandedChange = { expanded -> firstExpanded = expanded }, onExpandedChange = { expanded -> firstExpanded = expanded },
compactText = { Text(text = "Compact view") }, compactText = { Text(text = "Compact view") },
expandedText = { Text(text = "Expanded view") } expandedText = { Text(text = "Expanded view") },
) )
var secondExpanded by remember { mutableStateOf(true) } var secondExpanded by remember { mutableStateOf(true) }
NiaViewToggleButton( NiaViewToggleButton(
expanded = secondExpanded, expanded = secondExpanded,
onExpandedChange = { expanded -> secondExpanded = expanded }, onExpandedChange = { expanded -> secondExpanded = expanded },
compactText = { Text(text = "Compact view") }, compactText = { Text(text = "Compact view") },
expandedText = { Text(text = "Expanded view") } expandedText = { Text(text = "Expanded view") },
) )
NiaViewToggleButton( NiaViewToggleButton(
expanded = false, expanded = false,
onExpandedChange = {}, onExpandedChange = {},
compactText = { Text(text = "Disabled") }, compactText = { Text(text = "Disabled") },
expandedText = { Text(text = "Disabled") }, expandedText = { Text(text = "Disabled") },
enabled = false enabled = false,
) )
} }
} }
@ -330,7 +330,7 @@ fun NiaCatalog() {
text = { Text(text = "Topic 1".uppercase()) }, text = { Text(text = "Topic 1".uppercase()) },
followText = { Text(text = "Follow") }, followText = { Text(text = "Follow") },
unFollowText = { Text(text = "Unfollow") }, unFollowText = { Text(text = "Unfollow") },
browseText = { Text(text = "Browse topic") } browseText = { Text(text = "Browse topic") },
) )
var secondFollowed by remember { mutableStateOf(true) } var secondFollowed by remember { mutableStateOf(true) }
NiaTopicTag( NiaTopicTag(
@ -345,7 +345,7 @@ fun NiaCatalog() {
text = { Text(text = "Topic 2".uppercase()) }, text = { Text(text = "Topic 2".uppercase()) },
followText = { Text(text = "Follow") }, followText = { Text(text = "Follow") },
unFollowText = { Text(text = "Unfollow") }, unFollowText = { Text(text = "Unfollow") },
browseText = { Text(text = "Browse topic") } browseText = { Text(text = "Browse topic") },
) )
NiaTopicTag( NiaTopicTag(
expanded = false, expanded = false,
@ -355,7 +355,7 @@ fun NiaCatalog() {
onUnfollowClick = {}, onUnfollowClick = {},
onBrowseClick = {}, onBrowseClick = {},
text = { Text(text = "Disabled".uppercase()) }, text = { Text(text = "Disabled".uppercase()) },
enabled = false enabled = false,
) )
} }
} }
@ -368,7 +368,7 @@ fun NiaCatalog() {
NiaTab( NiaTab(
selected = selectedTabIndex == index, selected = selectedTabIndex == index,
onClick = { selectedTabIndex = index }, onClick = { selectedTabIndex = index },
text = { Text(text = title) } text = { Text(text = title) },
) )
} }
} }
@ -380,12 +380,12 @@ fun NiaCatalog() {
val icons = listOf( val icons = listOf(
NiaIcons.UpcomingBorder, NiaIcons.UpcomingBorder,
NiaIcons.MenuBookBorder, NiaIcons.MenuBookBorder,
NiaIcons.BookmarksBorder NiaIcons.BookmarksBorder,
) )
val selectedIcons = listOf( val selectedIcons = listOf(
NiaIcons.Upcoming, NiaIcons.Upcoming,
NiaIcons.MenuBook, NiaIcons.MenuBook,
NiaIcons.Bookmarks NiaIcons.Bookmarks,
) )
val tagIcon = NiaIcons.Tag val tagIcon = NiaIcons.Tag
NiaNavigationBar { NiaNavigationBar {
@ -397,7 +397,7 @@ fun NiaCatalog() {
} else { } else {
Icon( Icon(
painter = painterResource(id = icons[index]), painter = painterResource(id = icons[index]),
contentDescription = item contentDescription = item,
) )
} }
}, },
@ -407,13 +407,13 @@ fun NiaCatalog() {
} else { } else {
Icon( Icon(
painter = painterResource(id = selectedIcons[index]), painter = painterResource(id = selectedIcons[index]),
contentDescription = item contentDescription = item,
) )
} }
}, },
label = { Text(item) }, label = { Text(item) },
selected = selectedItem == index, selected = selectedItem == index,
onClick = { selectedItem = index } onClick = { selectedItem = index },
) )
} }
} }

@ -29,10 +29,6 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR
import com.google.samples.apps.nowinandroid.feature.interests.R as FeatureInterestsR
import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR
import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
@ -40,6 +36,10 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR
import com.google.samples.apps.nowinandroid.feature.interests.R as FeatureInterestsR
import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR
/** /**
* Tests all the navigation flows that are handled by the navigation library. * Tests all the navigation flows that are handled by the navigation library.
@ -57,7 +57,8 @@ class NavigationTest {
* Create a temporary folder used to create a Data Store file. This guarantees that * Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash. * the file is removed in between each test, preventing a crash.
*/ */
@BindValue @get:Rule(order = 1) @BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/** /**
@ -165,7 +166,6 @@ class NavigationTest {
@Test @Test
fun topLevelDestinations_showTopBarWithTitle() { fun topLevelDestinations_showTopBarWithTitle() {
composeTestRule.apply { composeTestRule.apply {
// Verify that the top bar contains the app name on the first screen. // Verify that the top bar contains the app name on the first screen.
onNodeWithText(appName).assertExists() onNodeWithText(appName).assertExists()
@ -207,7 +207,6 @@ class NavigationTest {
@Test @Test
fun whenSettingsDialogDismissed_previousScreenIsDisplayed() { fun whenSettingsDialogDismissed_previousScreenIsDisplayed() {
composeTestRule.apply { composeTestRule.apply {
// Navigate to the saved screen, open the settings dialog, then close it. // Navigate to the saved screen, open the settings dialog, then close it.
onNodeWithText(saved).performClick() onNodeWithText(saved).performClick()
onNodeWithContentDescription(settings).performClick() onNodeWithContentDescription(settings).performClick()

@ -30,11 +30,11 @@ import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActi
import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import javax.inject.Inject
/** /**
* Tests that the navigation UI is rendered correctly on different screen sizes. * Tests that the navigation UI is rendered correctly on different screen sizes.
@ -53,7 +53,8 @@ class NavigationUiTest {
* Create a temporary folder used to create a Data Store file. This guarantees that * Create a temporary folder used to create a Data Store file. This guarantees that
* the file is removed in between each test, preventing a crash. * the file is removed in between each test, preventing a crash.
*/ */
@BindValue @get:Rule(order = 1) @BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/** /**
@ -77,9 +78,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -96,9 +97,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -115,9 +116,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -134,9 +135,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -153,9 +154,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -172,9 +173,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -191,9 +192,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -210,9 +211,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }
@ -229,9 +230,9 @@ class NavigationUiTest {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize( windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight) DpSize(maxWidth, maxHeight),
), ),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
} }

@ -31,15 +31,15 @@ import androidx.navigation.compose.composable
import androidx.navigation.createGraph import androidx.navigation.createGraph
import androidx.navigation.testing.TestNavHostController import androidx.navigation.testing.TestNavHostController
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/** /**
* Tests [NiaAppState]. * Tests [NiaAppState].
@ -70,7 +70,7 @@ class NiaAppStateTest {
windowSizeClass = getCompactWindowClass(), windowSizeClass = getCompactWindowClass(),
navController = navController, navController = navController,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
coroutineScope = backgroundScope coroutineScope = backgroundScope,
) )
} }
@ -91,7 +91,7 @@ class NiaAppStateTest {
composeTestRule.setContent { composeTestRule.setContent {
state = rememberNiaAppState( state = rememberNiaAppState(
windowSizeClass = getCompactWindowClass(), windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor networkMonitor = networkMonitor,
) )
} }
@ -108,7 +108,7 @@ class NiaAppStateTest {
windowSizeClass = getCompactWindowClass(), windowSizeClass = getCompactWindowClass(),
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
coroutineScope = backgroundScope coroutineScope = backgroundScope,
) )
} }
@ -123,7 +123,7 @@ class NiaAppStateTest {
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)), windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
coroutineScope = backgroundScope coroutineScope = backgroundScope,
) )
} }
@ -133,13 +133,12 @@ class NiaAppStateTest {
@Test @Test
fun niaAppState_showNavRail_large() = runTest { fun niaAppState_showNavRail_large() = runTest {
composeTestRule.setContent { composeTestRule.setContent {
state = NiaAppState( state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)), windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
coroutineScope = backgroundScope coroutineScope = backgroundScope,
) )
} }
@ -149,13 +148,12 @@ class NiaAppStateTest {
@Test @Test
fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) { fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) {
composeTestRule.setContent { composeTestRule.setContent {
state = NiaAppState( state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)), windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
coroutineScope = backgroundScope coroutineScope = backgroundScope,
) )
} }
@ -163,7 +161,7 @@ class NiaAppStateTest {
networkMonitor.setConnected(false) networkMonitor.setConnected(false)
assertEquals( assertEquals(
true, true,
state.isOffline.value state.isOffline.value,
) )
} }

@ -43,10 +43,10 @@ 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.ThemeBrand
import com.google.samples.apps.nowinandroid.ui.NiaApp import com.google.samples.apps.nowinandroid.ui.NiaApp
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint @AndroidEntryPoint
@ -107,7 +107,7 @@ class MainActivity : ComponentActivity() {
NiaTheme( NiaTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(uiState), androidTheme = shouldUseAndroidTheme(uiState),
disableDynamicTheming = shouldDisableDynamicTheming(uiState) disableDynamicTheming = shouldDisableDynamicTheming(uiState),
) { ) {
NiaApp( NiaApp(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,

@ -23,22 +23,22 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainActivityViewModel @Inject constructor( class MainActivityViewModel @Inject constructor(
userDataRepository: UserDataRepository userDataRepository: UserDataRepository,
) : ViewModel() { ) : ViewModel() {
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map { val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map {
Success(it) Success(it)
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
initialValue = Loading, initialValue = Loading,
started = SharingStarted.WhileSubscribed(5_000) started = SharingStarted.WhileSubscribed(5_000),
) )
} }

@ -47,7 +47,7 @@ object JankStatsModule {
@Provides @Provides
fun providesJankStats( fun providesJankStats(
window: Window, window: Window,
frameListener: JankStats.OnFrameListener frameListener: JankStats.OnFrameListener,
): JankStats { ): JankStats {
return JankStats.createAndTrack(window, frameListener) return JankStats.createAndTrack(window, frameListener)
} }

@ -39,7 +39,7 @@ fun NiaNavHost(
navController: NavHostController, navController: NavHostController,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startDestination: String = forYouNavigationRoute startDestination: String = forYouNavigationRoute,
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
@ -54,7 +54,7 @@ fun NiaNavHost(
}, },
nestedGraphs = { nestedGraphs = {
topicScreen(onBackClick) topicScreen(onBackClick)
} },
) )
} }
} }

@ -34,24 +34,24 @@ enum class TopLevelDestination(
val selectedIcon: Icon, val selectedIcon: Icon,
val unselectedIcon: Icon, val unselectedIcon: Icon,
val iconTextId: Int, val iconTextId: Int,
val titleTextId: Int val titleTextId: Int,
) { ) {
FOR_YOU( FOR_YOU(
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming), selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder), unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = forYouR.string.for_you, iconTextId = forYouR.string.for_you,
titleTextId = R.string.app_name titleTextId = R.string.app_name,
), ),
BOOKMARKS( BOOKMARKS(
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks), selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder), unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = bookmarksR.string.saved, iconTextId = bookmarksR.string.saved,
titleTextId = bookmarksR.string.saved titleTextId = bookmarksR.string.saved,
), ),
INTERESTS( INTERESTS(
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3), selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3), unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interestsR.string.interests, iconTextId = interestsR.string.interests,
titleTextId = interestsR.string.interests titleTextId = interestsR.string.interests,
) ),
} }

@ -68,16 +68,16 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVec
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalLayoutApi::class, ExperimentalLayoutApi::class,
ExperimentalComposeUiApi::class, ExperimentalComposeUiApi::class,
ExperimentalLifecycleComposeApi::class ExperimentalLifecycleComposeApi::class,
) )
@Composable @Composable
fun NiaApp( fun NiaApp(
@ -85,7 +85,7 @@ fun NiaApp(
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
appState: NiaAppState = rememberNiaAppState( appState: NiaAppState = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
windowSizeClass = windowSizeClass windowSizeClass = windowSizeClass,
), ),
) { ) {
val shouldShowGradientBackground = val shouldShowGradientBackground =
@ -106,15 +106,17 @@ fun NiaApp(
// If user is not connected to the internet show a snack bar to inform them. // If user is not connected to the internet show a snack bar to inform them.
val notConnectedMessage = stringResource(R.string.not_connected) val notConnectedMessage = stringResource(R.string.not_connected)
LaunchedEffect(isOffline) { LaunchedEffect(isOffline) {
if (isOffline) snackbarHostState.showSnackbar( if (isOffline) {
snackbarHostState.showSnackbar(
message = notConnectedMessage, message = notConnectedMessage,
duration = Indefinite duration = Indefinite,
) )
} }
}
if (appState.shouldShowSettingsDialog) { if (appState.shouldShowSettingsDialog) {
SettingsDialog( SettingsDialog(
onDismiss = { appState.setShowSettingsDialog(false) } onDismiss = { appState.setShowSettingsDialog(false) },
) )
} }
@ -132,10 +134,10 @@ fun NiaApp(
destinations = appState.topLevelDestinations, destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination, onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination, currentDestination = appState.currentDestination,
modifier = Modifier.testTag("NiaBottomBar") modifier = Modifier.testTag("NiaBottomBar"),
) )
} }
} },
) { padding -> ) { padding ->
Row( Row(
Modifier Modifier
@ -144,9 +146,9 @@ fun NiaApp(
.consumedWindowInsets(padding) .consumedWindowInsets(padding)
.windowInsetsPadding( .windowInsetsPadding(
WindowInsets.safeDrawing.only( WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal WindowInsetsSides.Horizontal,
) ),
) ),
) { ) {
if (appState.shouldShowNavRail) { if (appState.shouldShowNavRail) {
NiaNavRail( NiaNavRail(
@ -155,7 +157,7 @@ fun NiaApp(
currentDestination = appState.currentDestination, currentDestination = appState.currentDestination,
modifier = Modifier modifier = Modifier
.testTag("NiaNavRail") .testTag("NiaNavRail")
.safeDrawingPadding() .safeDrawingPadding(),
) )
} }
@ -167,18 +169,18 @@ fun NiaApp(
titleRes = destination.titleTextId, titleRes = destination.titleTextId,
actionIcon = NiaIcons.Settings, actionIcon = NiaIcons.Settings,
actionIconContentDescription = stringResource( actionIconContentDescription = stringResource(
id = settingsR.string.top_app_bar_action_icon_description id = settingsR.string.top_app_bar_action_icon_description,
), ),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent containerColor = Color.Transparent,
), ),
onActionClick = { appState.setShowSettingsDialog(true) } onActionClick = { appState.setShowSettingsDialog(true) },
) )
} }
NiaNavHost( NiaNavHost(
navController = appState.navController, navController = appState.navController,
onBackClick = appState::onBackClick onBackClick = appState::onBackClick,
) )
} }
@ -212,15 +214,15 @@ private fun NiaNavRail(
when (icon) { when (icon) {
is ImageVectorIcon -> Icon( is ImageVectorIcon -> Icon(
imageVector = icon.imageVector, imageVector = icon.imageVector,
contentDescription = null contentDescription = null,
) )
is DrawableResourceIcon -> Icon( is DrawableResourceIcon -> Icon(
painter = painterResource(id = icon.id), painter = painterResource(id = icon.id),
contentDescription = null contentDescription = null,
) )
} }
}, },
label = { Text(stringResource(destination.iconTextId)) } label = { Text(stringResource(destination.iconTextId)) },
) )
} }
} }
@ -231,10 +233,10 @@ private fun NiaBottomBar(
destinations: List<TopLevelDestination>, destinations: List<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit, onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?, currentDestination: NavDestination?,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
NiaNavigationBar( NiaNavigationBar(
modifier = modifier modifier = modifier,
) { ) {
destinations.forEach { destination -> destinations.forEach { destination ->
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
@ -250,16 +252,16 @@ private fun NiaBottomBar(
when (icon) { when (icon) {
is ImageVectorIcon -> Icon( is ImageVectorIcon -> Icon(
imageVector = icon.imageVector, imageVector = icon.imageVector,
contentDescription = null contentDescription = null,
) )
is DrawableResourceIcon -> Icon( is DrawableResourceIcon -> Icon(
painter = painterResource(id = icon.id), painter = painterResource(id = icon.id),
contentDescription = null contentDescription = null,
) )
} }
}, },
label = { Text(stringResource(destination.iconTextId)) } label = { Text(stringResource(destination.iconTextId)) },
) )
} }
} }

@ -56,7 +56,7 @@ fun rememberNiaAppState(
windowSizeClass: WindowSizeClass, windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
coroutineScope: CoroutineScope = rememberCoroutineScope(), coroutineScope: CoroutineScope = rememberCoroutineScope(),
navController: NavHostController = rememberNavController() navController: NavHostController = rememberNavController(),
): NiaAppState { ): NiaAppState {
NavigationTrackingSideEffect(navController) NavigationTrackingSideEffect(navController)
return remember(navController, coroutineScope, windowSizeClass, networkMonitor) { return remember(navController, coroutineScope, windowSizeClass, networkMonitor) {
@ -98,7 +98,7 @@ class NiaAppState(
.stateIn( .stateIn(
scope = coroutineScope, scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),
initialValue = false initialValue = false,
) )
/** /**

@ -28,7 +28,7 @@ import androidx.test.uiautomator.HasChildrenOp.EXACTLY
*/ */
fun untilHasChildren( fun untilHasChildren(
childCount: Int = 1, childCount: Int = 1,
op: HasChildrenOp = AT_LEAST op: HasChildrenOp = AT_LEAST,
): UiObject2Condition<Boolean> { ): UiObject2Condition<Boolean> {
return object : UiObject2Condition<Boolean>() { return object : UiObject2Condition<Boolean>() {
override fun apply(element: UiObject2): Boolean { override fun apply(element: UiObject2): Boolean {
@ -44,5 +44,5 @@ fun untilHasChildren(
enum class HasChildrenOp { enum class HasChildrenOp {
AT_LEAST, AT_LEAST,
EXACTLY, EXACTLY,
AT_MOST AT_MOST,
} }

@ -47,7 +47,7 @@ class ScrollForYouFeedBenchmark {
// Start the app // Start the app
pressHome() pressHome()
startActivityAndWait() startActivityAndWait()
} },
) { ) {
forYouWaitForContent() forYouWaitForContent()
forYouSelectTopics() forYouSelectTopics()

@ -51,7 +51,7 @@ class TopicsScreenRecompositionBenchmark {
// Navigate to interests screen // Navigate to interests screen
device.findObject(By.text("Interests")).click() device.findObject(By.text("Interests")).click()
device.waitForIdle() device.waitForIdle()
} },
) { ) {
interestsWaitForTopics() interestsWaitForTopics()
repeat(3) { repeat(3) {

@ -66,7 +66,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
@Test @Test
fun startupBaselineProfileDisabled() = startup( fun startupBaselineProfileDisabled() = startup(
CompilationMode.Partial(baselineProfileMode = Disable, warmupIterations = 1) CompilationMode.Partial(baselineProfileMode = Disable, warmupIterations = 1),
) )
@Test @Test
@ -83,7 +83,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
startupMode = startupMode, startupMode = startupMode,
setupBlock = { setupBlock = {
pressHome() pressHome()
} },
) { ) {
startActivityAndWait() startActivityAndWait()
// Waits until the content is ready to capture Time To Full Display // Waits until the content is ready to capture Time To Full Display

@ -24,5 +24,5 @@ import kotlin.annotation.AnnotationRetention.RUNTIME
annotation class Dispatcher(val niaDispatcher: NiaDispatchers) annotation class Dispatcher(val niaDispatcher: NiaDispatchers)
enum class NiaDispatchers { enum class NiaDispatchers {
IO IO,
} }

@ -17,10 +17,10 @@
package com.google.samples.apps.nowinandroid.core.result package com.google.samples.apps.nowinandroid.core.result
import app.cash.turbine.test import app.cash.turbine.test
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class ResultKtTest { class ResultKtTest {
@ -38,11 +38,12 @@ class ResultKtTest {
when (val errorResult = awaitItem()) { when (val errorResult = awaitItem()) {
is Result.Error -> assertEquals( is Result.Error -> assertEquals(
"Test Done", "Test Done",
errorResult.exception?.message errorResult.exception?.message,
) )
Result.Loading, Result.Loading,
is Result.Success -> throw IllegalStateException( is Result.Success,
"The flow should have emitted an Error Result" -> throw IllegalStateException(
"The flow should have emitted an Error Result",
) )
} }

@ -17,9 +17,9 @@
package com.google.samples.apps.nowinandroid.core.data.test package com.google.samples.apps.nowinandroid.core.data.test
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
class AlwaysOnlineNetworkMonitor @Inject constructor() : NetworkMonitor { class AlwaysOnlineNetworkMonitor @Inject constructor() : NetworkMonitor {
override val isOnline: Flow<Boolean> = flowOf(true) override val isOnline: Flow<Boolean> = flowOf(true)

@ -32,26 +32,26 @@ import dagger.hilt.testing.TestInstallIn
@Module @Module
@TestInstallIn( @TestInstallIn(
components = [SingletonComponent::class], components = [SingletonComponent::class],
replaces = [DataModule::class] replaces = [DataModule::class],
) )
interface TestDataModule { interface TestDataModule {
@Binds @Binds
fun bindsTopicRepository( fun bindsTopicRepository(
fakeTopicsRepository: FakeTopicsRepository fakeTopicsRepository: FakeTopicsRepository,
): TopicsRepository ): TopicsRepository
@Binds @Binds
fun bindsNewsResourceRepository( fun bindsNewsResourceRepository(
fakeNewsRepository: FakeNewsRepository fakeNewsRepository: FakeNewsRepository,
): NewsRepository ): NewsRepository
@Binds @Binds
fun bindsUserDataRepository( fun bindsUserDataRepository(
userDataRepository: FakeUserDataRepository userDataRepository: FakeUserDataRepository,
): UserDataRepository ): UserDataRepository
@Binds @Binds
fun bindsNetworkMonitor( fun bindsNetworkMonitor(
networkMonitor: AlwaysOnlineNetworkMonitor networkMonitor: AlwaysOnlineNetworkMonitor,
): NetworkMonitor ): NetworkMonitor
} }

@ -19,9 +19,9 @@ package com.google.samples.apps.nowinandroid.core.data
import android.util.Log import android.util.Log
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlin.coroutines.cancellation.CancellationException
/** /**
* Interface marker for a class that manages synchronization between local data and a remote * Interface marker for a class that manages synchronization between local data and a remote
@ -62,7 +62,7 @@ private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> =
Log.i( Log.i(
"suspendRunCatching", "suspendRunCatching",
"Failed to evaluate a suspendRunCatchingBlock. Returning failure Result", "Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
exception exception,
) )
Result.failure(exception) Result.failure(exception)
} }
@ -116,10 +116,10 @@ fun <T1, T2, T3, T4, T5, T6, R> combine(
flow4: Flow<T4>, flow4: Flow<T4>,
flow5: Flow<T5>, flow5: Flow<T5>,
flow6: Flow<T6>, flow6: Flow<T6>,
transform: suspend (T1, T2, T3, T4, T5, T6) -> R transform: suspend (T1, T2, T3, T4, T5, T6) -> R,
): Flow<R> = combine( ): Flow<R> = combine(
combine(flow, flow2, flow3, ::Triple), combine(flow, flow2, flow3, ::Triple),
combine(flow4, flow5, flow6, ::Triple) combine(flow4, flow5, flow6, ::Triple),
) { t1, t2 -> ) { t1, t2 ->
transform( transform(
t1.first, t1.first,
@ -127,6 +127,6 @@ fun <T1, T2, T3, T4, T5, T6, R> combine(
t1.third, t1.third,
t2.first, t2.first,
t2.second, t2.second,
t2.third t2.third,
) )
} }

@ -35,21 +35,21 @@ interface DataModule {
@Binds @Binds
fun bindsTopicRepository( fun bindsTopicRepository(
topicsRepository: OfflineFirstTopicsRepository topicsRepository: OfflineFirstTopicsRepository,
): TopicsRepository ): TopicsRepository
@Binds @Binds
fun bindsNewsResourceRepository( fun bindsNewsResourceRepository(
newsRepository: OfflineFirstNewsRepository newsRepository: OfflineFirstNewsRepository,
): NewsRepository ): NewsRepository
@Binds @Binds
fun bindsUserDataRepository( fun bindsUserDataRepository(
userDataRepository: OfflineFirstUserDataRepository userDataRepository: OfflineFirstUserDataRepository,
): UserDataRepository ): UserDataRepository
@Binds @Binds
fun bindsNetworkMonitor( fun bindsNetworkMonitor(
networkMonitor: ConnectivityManagerNetworkMonitor networkMonitor: ConnectivityManagerNetworkMonitor,
): NetworkMonitor ): NetworkMonitor
} }

@ -62,6 +62,6 @@ fun NetworkNewsResource.topicCrossReferences(): List<NewsResourceTopicCrossRef>
topics.map { topicId -> topics.map { topicId ->
NewsResourceTopicCrossRef( NewsResourceTopicCrossRef(
newsResourceId = id, newsResourceId = id,
topicId = topicId topicId = topicId,
) )
} }

@ -25,5 +25,5 @@ fun NetworkTopic.asEntity() = TopicEntity(
shortDescription = shortDescription, shortDescription = shortDescription,
longDescription = longDescription, longDescription = longDescription,
url = url, url = url,
imageUrl = imageUrl imageUrl = imageUrl,
) )

@ -30,9 +30,9 @@ import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject
/** /**
* Disk storage backed implementation of the [NewsRepository]. * Disk storage backed implementation of the [NewsRepository].
@ -49,9 +49,9 @@ class OfflineFirstNewsRepository @Inject constructor(
.map { it.map(PopulatedNewsResource::asExternalModel) } .map { it.map(PopulatedNewsResource::asExternalModel) }
override fun getNewsResources( override fun getNewsResources(
filterTopicIds: Set<String> filterTopicIds: Set<String>,
): Flow<List<NewsResource>> = newsResourceDao.getNewsResources( ): Flow<List<NewsResource>> = newsResourceDao.getNewsResources(
filterTopicIds = filterTopicIds filterTopicIds = filterTopicIds,
) )
.map { it.map(PopulatedNewsResource::asExternalModel) } .map { it.map(PopulatedNewsResource::asExternalModel) }
@ -74,18 +74,18 @@ class OfflineFirstNewsRepository @Inject constructor(
topicEntities = networkNewsResources topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells) .map(NetworkNewsResource::topicEntityShells)
.flatten() .flatten()
.distinctBy(TopicEntity::id) .distinctBy(TopicEntity::id),
) )
newsResourceDao.upsertNewsResources( newsResourceDao.upsertNewsResources(
newsResourceEntities = networkNewsResources newsResourceEntities = networkNewsResources
.map(NetworkNewsResource::asEntity) .map(NetworkNewsResource::asEntity),
) )
newsResourceDao.insertOrIgnoreTopicCrossRefEntities( newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences = networkNewsResources newsResourceTopicCrossReferences = networkNewsResources
.map(NetworkNewsResource::topicCrossReferences) .map(NetworkNewsResource::topicCrossReferences)
.distinct() .distinct()
.flatten() .flatten(),
) )
} },
) )
} }

@ -26,9 +26,9 @@ import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject
/** /**
* Disk storage backed implementation of the [TopicsRepository]. * Disk storage backed implementation of the [TopicsRepository].
@ -59,8 +59,8 @@ class OfflineFirstTopicsRepository @Inject constructor(
modelUpdater = { changedIds -> modelUpdater = { changedIds ->
val networkTopics = network.getTopics(ids = changedIds) val networkTopics = network.getTopics(ids = changedIds)
topicDao.upsertTopics( topicDao.upsertTopics(
entities = networkTopics.map(NetworkTopic::asEntity) entities = networkTopics.map(NetworkTopic::asEntity),
) )
} },
) )
} }

@ -20,11 +20,11 @@ import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSou
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig 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.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class OfflineFirstUserDataRepository @Inject constructor( class OfflineFirstUserDataRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource private val niaPreferencesDataSource: NiaPreferencesDataSource,
) : UserDataRepository { ) : UserDataRepository {
override val userData: Flow<UserData> = override val userData: Flow<UserData> =

@ -26,11 +26,11 @@ import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
/** /**
* Fake implementation of the [NewsRepository] that retrieves the news resources from a JSON String. * Fake implementation of the [NewsRepository] that retrieves the news resources from a JSON String.
@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.flowOn
*/ */
class FakeNewsRepository @Inject constructor( class FakeNewsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val datasource: FakeNiaNetworkDataSource private val datasource: FakeNiaNetworkDataSource,
) : NewsRepository { ) : NewsRepository {
override fun getNewsResources(): Flow<List<NewsResource>> = override fun getNewsResources(): Flow<List<NewsResource>> =
@ -48,7 +48,7 @@ class FakeNewsRepository @Inject constructor(
emit( emit(
datasource.getNewsResources() datasource.getNewsResources()
.map(NetworkNewsResource::asEntity) .map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel) .map(NewsResourceEntity::asExternalModel),
) )
}.flowOn(ioDispatcher) }.flowOn(ioDispatcher)
@ -61,7 +61,7 @@ class FakeNewsRepository @Inject constructor(
.getNewsResources() .getNewsResources()
.filter { it.topics.intersect(filterTopicIds).isNotEmpty() } .filter { it.topics.intersect(filterTopicIds).isNotEmpty() }
.map(NetworkNewsResource::asEntity) .map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel) .map(NewsResourceEntity::asExternalModel),
) )
}.flowOn(ioDispatcher) }.flowOn(ioDispatcher)

@ -22,12 +22,12 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject
/** /**
* Fake implementation of the [TopicsRepository] that retrieves the topics from a JSON String, and * Fake implementation of the [TopicsRepository] that retrieves the topics from a JSON String, and
@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.map
*/ */
class FakeTopicsRepository @Inject constructor( class FakeTopicsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val datasource: FakeNiaNetworkDataSource private val datasource: FakeNiaNetworkDataSource,
) : TopicsRepository { ) : TopicsRepository {
override fun getTopics(): Flow<List<Topic>> = flow { override fun getTopics(): Flow<List<Topic>> = flow {
emit( emit(
@ -49,9 +49,9 @@ class FakeTopicsRepository @Inject constructor(
shortDescription = it.shortDescription, shortDescription = it.shortDescription,
longDescription = it.longDescription, longDescription = it.longDescription,
url = it.url, url = it.url,
imageUrl = it.imageUrl imageUrl = it.imageUrl,
) )
} },
) )
}.flowOn(ioDispatcher) }.flowOn(ioDispatcher)

@ -21,8 +21,8 @@ import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSou
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig 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.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/** /**
* Fake implementation of the [UserDataRepository] that returns hardcoded user data. * Fake implementation of the [UserDataRepository] that returns hardcoded user data.

@ -26,14 +26,14 @@ import android.os.Build.VERSION
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import javax.inject.Inject
class ConnectivityManagerNetworkMonitor @Inject constructor( class ConnectivityManagerNetworkMonitor @Inject constructor(
@ApplicationContext private val context: Context @ApplicationContext private val context: Context,
) : NetworkMonitor { ) : NetworkMonitor {
override val isOnline: Flow<Boolean> = callbackFlow { override val isOnline: Flow<Boolean> = callbackFlow {
val connectivityManager = context.getSystemService<ConnectivityManager>() val connectivityManager = context.getSystemService<ConnectivityManager>()
@ -54,7 +54,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
override fun onCapabilitiesChanged( override fun onCapabilitiesChanged(
network: Network, network: Network,
networkCapabilities: NetworkCapabilities networkCapabilities: NetworkCapabilities,
) { ) {
channel.trySend(connectivityManager.isCurrentlyConnected()) channel.trySend(connectivityManager.isCurrentlyConnected())
} }
@ -64,7 +64,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
Builder() Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build(), .build(),
callback callback,
) )
channel.trySend(connectivityManager.isCurrentlyConnected()) channel.trySend(connectivityManager.isCurrentlyConnected())

@ -20,9 +20,9 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Art
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlin.test.assertEquals
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class NetworkEntityKtTest { class NetworkEntityKtTest {

@ -35,13 +35,13 @@ import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferen
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
class OfflineFirstNewsRepositoryTest { class OfflineFirstNewsRepositoryTest {
@ -65,8 +65,8 @@ class OfflineFirstNewsRepositoryTest {
network = TestNiaNetworkDataSource() network = TestNiaNetworkDataSource()
synchronizer = TestSynchronizer( synchronizer = TestSynchronizer(
NiaPreferencesDataSource( NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore() tmpFolder.testUserPreferencesDataStore(),
) ),
) )
subject = OfflineFirstNewsRepository( subject = OfflineFirstNewsRepository(
@ -84,7 +84,7 @@ class OfflineFirstNewsRepositoryTest {
.first() .first()
.map(PopulatedNewsResource::asExternalModel), .map(PopulatedNewsResource::asExternalModel),
subject.getNewsResources() subject.getNewsResources()
.first() .first(),
) )
} }
@ -100,7 +100,7 @@ class OfflineFirstNewsRepositoryTest {
subject.getNewsResources( subject.getNewsResources(
filterTopicIds = filteredInterestsIds, filterTopicIds = filteredInterestsIds,
) )
.first() .first(),
) )
assertEquals( assertEquals(
@ -108,7 +108,7 @@ class OfflineFirstNewsRepositoryTest {
subject.getNewsResources( subject.getNewsResources(
filterTopicIds = nonPresentInterestsIds, filterTopicIds = nonPresentInterestsIds,
) )
.first() .first(),
) )
} }
@ -127,13 +127,13 @@ class OfflineFirstNewsRepositoryTest {
assertEquals( assertEquals(
newsResourcesFromNetwork.map(NewsResource::id), newsResourcesFromNetwork.map(NewsResource::id),
newsResourcesFromDb.map(NewsResource::id) newsResourcesFromDb.map(NewsResource::id),
) )
// After sync version should be updated // After sync version should be updated
assertEquals( assertEquals(
network.latestChangeListVersion(CollectionType.NewsResources), network.latestChangeListVersion(CollectionType.NewsResources),
synchronizer.getChangeListVersions().newsResourceVersion synchronizer.getChangeListVersions().newsResourceVersion,
) )
} }
@ -155,7 +155,7 @@ class OfflineFirstNewsRepositoryTest {
network.editCollection( network.editCollection(
collectionType = CollectionType.NewsResources, collectionType = CollectionType.NewsResources,
id = it, id = it,
isDelete = true isDelete = true,
) )
} }
@ -168,13 +168,13 @@ class OfflineFirstNewsRepositoryTest {
// Assert that items marked deleted on the network have been deleted locally // Assert that items marked deleted on the network have been deleted locally
assertEquals( assertEquals(
newsResourcesFromNetwork.map(NewsResource::id) - deletedItems, newsResourcesFromNetwork.map(NewsResource::id) - deletedItems,
newsResourcesFromDb.map(NewsResource::id) newsResourcesFromDb.map(NewsResource::id),
) )
// After sync version should be updated // After sync version should be updated
assertEquals( assertEquals(
network.latestChangeListVersion(CollectionType.NewsResources), network.latestChangeListVersion(CollectionType.NewsResources),
synchronizer.getChangeListVersions().newsResourceVersion synchronizer.getChangeListVersions().newsResourceVersion,
) )
} }
@ -190,7 +190,7 @@ class OfflineFirstNewsRepositoryTest {
val changeList = network.changeListsAfter( val changeList = network.changeListsAfter(
CollectionType.NewsResources, CollectionType.NewsResources,
version = 7 version = 7,
) )
val changeListIds = changeList val changeListIds = changeList
.map(NetworkChangeList::id) .map(NetworkChangeList::id)
@ -207,13 +207,13 @@ class OfflineFirstNewsRepositoryTest {
assertEquals( assertEquals(
newsResourcesFromNetwork.map(NewsResource::id), newsResourcesFromNetwork.map(NewsResource::id),
newsResourcesFromDb.map(NewsResource::id) newsResourcesFromDb.map(NewsResource::id),
) )
// After sync version should be updated // After sync version should be updated
assertEquals( assertEquals(
changeList.last().changeListVersion, changeList.last().changeListVersion,
synchronizer.getChangeListVersions().newsResourceVersion synchronizer.getChangeListVersions().newsResourceVersion,
) )
} }
@ -228,7 +228,7 @@ class OfflineFirstNewsRepositoryTest {
.flatten() .flatten()
.distinctBy(TopicEntity::id), .distinctBy(TopicEntity::id),
topicDao.getTopicEntities() topicDao.getTopicEntities()
.first() .first(),
) )
} }
@ -242,7 +242,7 @@ class OfflineFirstNewsRepositoryTest {
.map(NetworkNewsResource::topicCrossReferences) .map(NetworkNewsResource::topicCrossReferences)
.distinct() .distinct()
.flatten(), .flatten(),
newsResourceDao.topicCrossReferences newsResourceDao.topicCrossReferences,
) )
} }
} }

@ -28,13 +28,13 @@ import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSou
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
class OfflineFirstTopicsRepositoryTest { class OfflineFirstTopicsRepositoryTest {
@ -56,13 +56,13 @@ class OfflineFirstTopicsRepositoryTest {
topicDao = TestTopicDao() topicDao = TestTopicDao()
network = TestNiaNetworkDataSource() network = TestNiaNetworkDataSource()
niaPreferences = NiaPreferencesDataSource( niaPreferences = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore() tmpFolder.testUserPreferencesDataStore(),
) )
synchronizer = TestSynchronizer(niaPreferences) synchronizer = TestSynchronizer(niaPreferences)
subject = OfflineFirstTopicsRepository( subject = OfflineFirstTopicsRepository(
topicDao = topicDao, topicDao = topicDao,
network = network network = network,
) )
} }
@ -74,7 +74,7 @@ class OfflineFirstTopicsRepositoryTest {
.first() .first()
.map(TopicEntity::asExternalModel), .map(TopicEntity::asExternalModel),
subject.getTopics() subject.getTopics()
.first() .first(),
) )
} }
@ -91,13 +91,13 @@ class OfflineFirstTopicsRepositoryTest {
assertEquals( assertEquals(
networkTopics.map(TopicEntity::id), networkTopics.map(TopicEntity::id),
dbTopics.map(TopicEntity::id) dbTopics.map(TopicEntity::id),
) )
// After sync version should be updated // After sync version should be updated
assertEquals( assertEquals(
network.latestChangeListVersion(CollectionType.Topics), network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion synchronizer.getChangeListVersions().topicVersion,
) )
} }
@ -121,13 +121,13 @@ class OfflineFirstTopicsRepositoryTest {
assertEquals( assertEquals(
networkTopics.map(TopicEntity::id), networkTopics.map(TopicEntity::id),
dbTopics.map(TopicEntity::id) dbTopics.map(TopicEntity::id),
) )
// After sync version should be updated // After sync version should be updated
assertEquals( assertEquals(
network.latestChangeListVersion(CollectionType.Topics), network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion synchronizer.getChangeListVersions().topicVersion,
) )
} }
@ -149,7 +149,7 @@ class OfflineFirstTopicsRepositoryTest {
network.editCollection( network.editCollection(
collectionType = CollectionType.Topics, collectionType = CollectionType.Topics,
id = it, id = it,
isDelete = true isDelete = true,
) )
} }
@ -162,13 +162,13 @@ class OfflineFirstTopicsRepositoryTest {
// Assert that items marked deleted on the network have been deleted locally // Assert that items marked deleted on the network have been deleted locally
assertEquals( assertEquals(
networkTopics.map(Topic::id) - deletedItems, networkTopics.map(Topic::id) - deletedItems,
dbTopics.map(Topic::id) dbTopics.map(Topic::id),
) )
// After sync version should be updated // After sync version should be updated
assertEquals( assertEquals(
network.latestChangeListVersion(CollectionType.Topics), network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion synchronizer.getChangeListVersions().topicVersion,
) )
} }
} }

@ -21,9 +21,6 @@ import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferen
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig 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.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -31,6 +28,9 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class OfflineFirstUserDataRepositoryTest { class OfflineFirstUserDataRepositoryTest {
private lateinit var subject: OfflineFirstUserDataRepository private lateinit var subject: OfflineFirstUserDataRepository
@ -43,11 +43,11 @@ class OfflineFirstUserDataRepositoryTest {
@Before @Before
fun setup() { fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource( niaPreferencesDataSource = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore() tmpFolder.testUserPreferencesDataStore(),
) )
subject = OfflineFirstUserDataRepository( subject = OfflineFirstUserDataRepository(
niaPreferencesDataSource = niaPreferencesDataSource niaPreferencesDataSource = niaPreferencesDataSource,
) )
} }
@ -61,9 +61,9 @@ class OfflineFirstUserDataRepositoryTest {
themeBrand = ThemeBrand.DEFAULT, themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
useDynamicColor = false, useDynamicColor = false,
shouldHideOnboarding = false shouldHideOnboarding = false,
), ),
subject.userData.first() subject.userData.first(),
) )
} }
@ -76,7 +76,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0"), setOf("0"),
subject.userData subject.userData
.map { it.followedTopics } .map { it.followedTopics }
.first() .first(),
) )
subject.toggleFollowedTopicId(followedTopicId = "1", followed = true) subject.toggleFollowedTopicId(followedTopicId = "1", followed = true)
@ -85,7 +85,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0", "1"), setOf("0", "1"),
subject.userData subject.userData
.map { it.followedTopics } .map { it.followedTopics }
.first() .first(),
) )
assertEquals( assertEquals(
@ -94,7 +94,7 @@ class OfflineFirstUserDataRepositoryTest {
.first(), .first(),
subject.userData subject.userData
.map { it.followedTopics } .map { it.followedTopics }
.first() .first(),
) )
} }
@ -107,7 +107,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("1", "2"), setOf("1", "2"),
subject.userData subject.userData
.map { it.followedTopics } .map { it.followedTopics }
.first() .first(),
) )
assertEquals( assertEquals(
@ -116,7 +116,7 @@ class OfflineFirstUserDataRepositoryTest {
.first(), .first(),
subject.userData subject.userData
.map { it.followedTopics } .map { it.followedTopics }
.first() .first(),
) )
} }
@ -129,7 +129,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0"), setOf("0"),
subject.userData subject.userData
.map { it.bookmarkedNewsResources } .map { it.bookmarkedNewsResources }
.first() .first(),
) )
subject.updateNewsResourceBookmark(newsResourceId = "1", bookmarked = true) subject.updateNewsResourceBookmark(newsResourceId = "1", bookmarked = true)
@ -138,7 +138,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0", "1"), setOf("0", "1"),
subject.userData subject.userData
.map { it.bookmarkedNewsResources } .map { it.bookmarkedNewsResources }
.first() .first(),
) )
assertEquals( assertEquals(
@ -147,7 +147,7 @@ class OfflineFirstUserDataRepositoryTest {
.first(), .first(),
subject.userData subject.userData
.map { it.bookmarkedNewsResources } .map { it.bookmarkedNewsResources }
.first() .first(),
) )
} }
@ -160,14 +160,14 @@ class OfflineFirstUserDataRepositoryTest {
ThemeBrand.ANDROID, ThemeBrand.ANDROID,
subject.userData subject.userData
.map { it.themeBrand } .map { it.themeBrand }
.first() .first(),
) )
assertEquals( assertEquals(
ThemeBrand.ANDROID, ThemeBrand.ANDROID,
niaPreferencesDataSource niaPreferencesDataSource
.userData .userData
.map { it.themeBrand } .map { it.themeBrand }
.first() .first(),
) )
} }
@ -180,14 +180,14 @@ class OfflineFirstUserDataRepositoryTest {
true, true,
subject.userData subject.userData
.map { it.useDynamicColor } .map { it.useDynamicColor }
.first() .first(),
) )
assertEquals( assertEquals(
true, true,
niaPreferencesDataSource niaPreferencesDataSource
.userData .userData
.map { it.useDynamicColor } .map { it.useDynamicColor }
.first() .first(),
) )
} }
@ -200,14 +200,14 @@ class OfflineFirstUserDataRepositoryTest {
DarkThemeConfig.DARK, DarkThemeConfig.DARK,
subject.userData subject.userData
.map { it.darkThemeConfig } .map { it.darkThemeConfig }
.first() .first(),
) )
assertEquals( assertEquals(
DarkThemeConfig.DARK, DarkThemeConfig.DARK,
niaPreferencesDataSource niaPreferencesDataSource
.userData .userData
.map { it.darkThemeConfig } .map { it.darkThemeConfig }
.first() .first(),
) )
} }

@ -24,12 +24,12 @@ import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSou
* Test synchronizer that delegates to [NiaPreferencesDataSource] * Test synchronizer that delegates to [NiaPreferencesDataSource]
*/ */
class TestSynchronizer( class TestSynchronizer(
private val niaPreferences: NiaPreferencesDataSource private val niaPreferences: NiaPreferencesDataSource,
) : Synchronizer { ) : Synchronizer {
override suspend fun getChangeListVersions(): ChangeListVersions = override suspend fun getChangeListVersions(): ChangeListVersions =
niaPreferences.getChangeListVersions() niaPreferences.getChangeListVersions()
override suspend fun updateChangeListVersions( override suspend fun updateChangeListVersions(
update: ChangeListVersions.() -> ChangeListVersions update: ChangeListVersions.() -> ChangeListVersions,
) = niaPreferences.updateChangeListVersion(update) ) = niaPreferences.updateChangeListVersion(update)
} }

@ -46,8 +46,8 @@ class TestNewsResourceDao : NewsResourceDao {
headerImageUrl = "headerImageUrl", headerImageUrl = "headerImageUrl",
type = Video, type = Video,
publishDate = Instant.fromEpochMilliseconds(1), publishDate = Instant.fromEpochMilliseconds(1),
) ),
) ),
) )
internal var topicCrossReferences: List<NewsResourceTopicCrossRef> = listOf() internal var topicCrossReferences: List<NewsResourceTopicCrossRef> = listOf()
@ -58,7 +58,7 @@ class TestNewsResourceDao : NewsResourceDao {
} }
override fun getNewsResources( override fun getNewsResources(
filterTopicIds: Set<String> filterTopicIds: Set<String>,
): Flow<List<PopulatedNewsResource>> = ): Flow<List<PopulatedNewsResource>> =
getNewsResources() getNewsResources()
.map { resources -> .map { resources ->
@ -68,7 +68,7 @@ class TestNewsResourceDao : NewsResourceDao {
} }
override suspend fun insertOrIgnoreNewsResources( override suspend fun insertOrIgnoreNewsResources(
entities: List<NewsResourceEntity> entities: List<NewsResourceEntity>,
): List<Long> { ): List<Long> {
entitiesStateFlow.value = entities entitiesStateFlow.value = entities
// Assume no conflicts on insert // Assume no conflicts on insert
@ -84,7 +84,7 @@ class TestNewsResourceDao : NewsResourceDao {
} }
override suspend fun insertOrIgnoreTopicCrossRefEntities( override suspend fun insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences: List<NewsResourceTopicCrossRef> newsResourceTopicCrossReferences: List<NewsResourceTopicCrossRef>,
) { ) {
topicCrossReferences = newsResourceTopicCrossReferences topicCrossReferences = newsResourceTopicCrossReferences
} }
@ -107,6 +107,6 @@ private fun NewsResourceEntity.asPopulatedNewsResource() = PopulatedNewsResource
longDescription = "long description", longDescription = "long description",
url = "URL", url = "URL",
imageUrl = "image URL", imageUrl = "image URL",
) ),
), ),
) )

@ -27,7 +27,7 @@ import kotlinx.serialization.json.Json
enum class CollectionType { enum class CollectionType {
Topics, Topics,
NewsResources NewsResources,
} }
/** /**
@ -37,7 +37,7 @@ class TestNiaNetworkDataSource : NiaNetworkDataSource {
private val source = FakeNiaNetworkDataSource( private val source = FakeNiaNetworkDataSource(
UnconfinedTestDispatcher(), UnconfinedTestDispatcher(),
Json { ignoreUnknownKeys = true } Json { ignoreUnknownKeys = true },
) )
private val allTopics = runBlocking { source.getTopics() } private val allTopics = runBlocking { source.getTopics() }
@ -54,13 +54,13 @@ class TestNiaNetworkDataSource : NiaNetworkDataSource {
override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> = override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> =
allTopics.matchIds( allTopics.matchIds(
ids = ids, ids = ids,
idGetter = NetworkTopic::id idGetter = NetworkTopic::id,
) )
override suspend fun getNewsResources(ids: List<String>?): List<NetworkNewsResource> = override suspend fun getNewsResources(ids: List<String>?): List<NetworkNewsResource> =
allNewsResources.matchIds( allNewsResources.matchIds(
ids = ids, ids = ids,
idGetter = NetworkNewsResource::id idGetter = NetworkNewsResource::id,
) )
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> = override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =
@ -102,7 +102,7 @@ fun List<NetworkChangeList>.after(version: Int?): List<NetworkChangeList> =
*/ */
private fun <T> List<T>.matchIds( private fun <T> List<T>.matchIds(
ids: List<String>?, ids: List<String>?,
idGetter: (T) -> String idGetter: (T) -> String,
) = when (ids) { ) = when (ids) {
null -> this null -> this
else -> ids.toSet().let { idSet -> this.filter { idSet.contains(idGetter(it)) } } else -> ids.toSet().let { idSet -> this.filter { idSet.contains(idGetter(it)) } }
@ -113,7 +113,7 @@ private fun <T> List<T>.matchIds(
* [after] simulates which models have changed by excluding items before it * [after] simulates which models have changed by excluding items before it
*/ */
private fun <T> List<T>.mapToChangeList( private fun <T> List<T>.mapToChangeList(
idGetter: (T) -> String idGetter: (T) -> String,
) = mapIndexed { index, item -> ) = mapIndexed { index, item ->
NetworkChangeList( NetworkChangeList(
id = idGetter(item), id = idGetter(item),

@ -37,8 +37,8 @@ class TestTopicDao : TopicDao {
longDescription = "long description", longDescription = "long description",
url = "URL", url = "URL",
imageUrl = "image URL", imageUrl = "image URL",
) ),
) ),
) )
override fun getTopicEntity(topicId: String): Flow<TopicEntity> { override fun getTopicEntity(topicId: String): Flow<TopicEntity> {

@ -19,9 +19,9 @@ package com.google.samples.apps.nowinandroid.core.database.model
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlin.test.assertEquals
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class PopulatedNewsResourceKtTest { class PopulatedNewsResourceKtTest {
@Test @Test
@ -44,7 +44,7 @@ class PopulatedNewsResourceKtTest {
longDescription = "long description", longDescription = "long description",
url = "URL", url = "URL",
imageUrl = "image URL", imageUrl = "image URL",
) ),
), ),
) )
val newsResource = populatedNewsResource.asExternalModel() val newsResource = populatedNewsResource.asExternalModel()
@ -66,10 +66,10 @@ class PopulatedNewsResourceKtTest {
longDescription = "long description", longDescription = "long description",
url = "URL", url = "URL",
imageUrl = "image URL", imageUrl = "image URL",
)
)
), ),
newsResource ),
),
newsResource,
) )
} }
} }

@ -17,8 +17,8 @@
package com.google.samples.apps.nowinandroid.core.database.util package com.google.samples.apps.nowinandroid.core.database.util
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlin.test.assertEquals
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class NewsResourceTypeConverterTest { class NewsResourceTypeConverterTest {
@ -26,7 +26,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_video() { fun test_room_news_resource_type_converter_for_video() {
assertEquals( assertEquals(
NewsResourceType.Video, NewsResourceType.Video,
NewsResourceTypeConverter().stringToNewsResourceType("Video 📺") NewsResourceTypeConverter().stringToNewsResourceType("Video 📺"),
) )
} }
@ -34,7 +34,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_article() { fun test_room_news_resource_type_converter_for_article() {
assertEquals( assertEquals(
NewsResourceType.Article, NewsResourceType.Article,
NewsResourceTypeConverter().stringToNewsResourceType("Article 📚") NewsResourceTypeConverter().stringToNewsResourceType("Article 📚"),
) )
} }
@ -42,7 +42,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_api_change() { fun test_room_news_resource_type_converter_for_api_change() {
assertEquals( assertEquals(
NewsResourceType.APIChange, NewsResourceType.APIChange,
NewsResourceTypeConverter().stringToNewsResourceType("API change") NewsResourceTypeConverter().stringToNewsResourceType("API change"),
) )
} }
@ -50,7 +50,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_codelab() { fun test_room_news_resource_type_converter_for_codelab() {
assertEquals( assertEquals(
NewsResourceType.Codelab, NewsResourceType.Codelab,
NewsResourceTypeConverter().stringToNewsResourceType("Codelab") NewsResourceTypeConverter().stringToNewsResourceType("Codelab"),
) )
} }
@ -58,7 +58,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_podcast() { fun test_room_news_resource_type_converter_for_podcast() {
assertEquals( assertEquals(
NewsResourceType.Podcast, NewsResourceType.Podcast,
NewsResourceTypeConverter().stringToNewsResourceType("Podcast 🎙") NewsResourceTypeConverter().stringToNewsResourceType("Podcast 🎙"),
) )
} }
@ -66,7 +66,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_docs() { fun test_room_news_resource_type_converter_for_docs() {
assertEquals( assertEquals(
NewsResourceType.Docs, NewsResourceType.Docs,
NewsResourceTypeConverter().stringToNewsResourceType("Docs 📑") NewsResourceTypeConverter().stringToNewsResourceType("Docs 📑"),
) )
} }
@ -74,7 +74,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_event() { fun test_room_news_resource_type_converter_for_event() {
assertEquals( assertEquals(
NewsResourceType.Event, NewsResourceType.Event,
NewsResourceTypeConverter().stringToNewsResourceType("Event 📆") NewsResourceTypeConverter().stringToNewsResourceType("Event 📆"),
) )
} }
@ -82,7 +82,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_dac() { fun test_room_news_resource_type_converter_for_dac() {
assertEquals( assertEquals(
NewsResourceType.DAC, NewsResourceType.DAC,
NewsResourceTypeConverter().stringToNewsResourceType("DAC") NewsResourceTypeConverter().stringToNewsResourceType("DAC"),
) )
} }
@ -90,7 +90,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_umm() { fun test_room_news_resource_type_converter_for_umm() {
assertEquals( assertEquals(
NewsResourceType.Unknown, NewsResourceType.Unknown,
NewsResourceTypeConverter().stringToNewsResourceType("umm") NewsResourceTypeConverter().stringToNewsResourceType("umm"),
) )
} }
} }

@ -25,12 +25,12 @@ import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopi
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class NewsResourceDaoTest { class NewsResourceDaoTest {
@ -43,7 +43,7 @@ class NewsResourceDaoTest {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder( db = Room.inMemoryDatabaseBuilder(
context, context,
NiaDatabase::class.java NiaDatabase::class.java,
).build() ).build()
newsResourceDao = db.newsResourceDao() newsResourceDao = db.newsResourceDao()
topicDao = db.topicDao() topicDao = db.topicDao()
@ -70,7 +70,7 @@ class NewsResourceDaoTest {
), ),
) )
newsResourceDao.upsertNewsResources( newsResourceDao.upsertNewsResources(
newsResourceEntities newsResourceEntities,
) )
val savedNewsResourceEntities = newsResourceDao.getNewsResources() val savedNewsResourceEntities = newsResourceDao.getNewsResources()
@ -80,7 +80,7 @@ class NewsResourceDaoTest {
listOf(3L, 2L, 1L, 0L), listOf(3L, 2L, 1L, 0L),
savedNewsResourceEntities.map { savedNewsResourceEntities.map {
it.asExternalModel().publishDate.toEpochMilliseconds() it.asExternalModel().publishDate.toEpochMilliseconds()
} },
) )
} }
@ -89,11 +89,11 @@ class NewsResourceDaoTest {
val topicEntities = listOf( val topicEntities = listOf(
testTopicEntity( testTopicEntity(
id = "1", id = "1",
name = "1" name = "1",
), ),
testTopicEntity( testTopicEntity(
id = "2", id = "2",
name = "2" name = "2",
), ),
) )
val newsResourceEntities = listOf( val newsResourceEntities = listOf(
@ -117,18 +117,18 @@ class NewsResourceDaoTest {
val newsResourceTopicCrossRefEntities = topicEntities.mapIndexed { index, topicEntity -> val newsResourceTopicCrossRefEntities = topicEntities.mapIndexed { index, topicEntity ->
NewsResourceTopicCrossRef( NewsResourceTopicCrossRef(
newsResourceId = index.toString(), newsResourceId = index.toString(),
topicId = topicEntity.id topicId = topicEntity.id,
) )
} }
topicDao.insertOrIgnoreTopics( topicDao.insertOrIgnoreTopics(
topicEntities = topicEntities topicEntities = topicEntities,
) )
newsResourceDao.upsertNewsResources( newsResourceDao.upsertNewsResources(
newsResourceEntities newsResourceEntities,
) )
newsResourceDao.insertOrIgnoreTopicCrossRefEntities( newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossRefEntities newsResourceTopicCrossRefEntities,
) )
val filteredNewsResources = newsResourceDao.getNewsResources( val filteredNewsResources = newsResourceDao.getNewsResources(
@ -139,7 +139,7 @@ class NewsResourceDaoTest {
assertEquals( assertEquals(
listOf("1", "0"), listOf("1", "0"),
filteredNewsResources.map { it.entity.id } filteredNewsResources.map { it.entity.id },
) )
} }
@ -169,7 +169,7 @@ class NewsResourceDaoTest {
val (toDelete, toKeep) = newsResourceEntities.partition { it.id.toInt() % 2 == 0 } val (toDelete, toKeep) = newsResourceEntities.partition { it.id.toInt() % 2 == 0 }
newsResourceDao.deleteNewsResources( newsResourceDao.deleteNewsResources(
toDelete.map(NewsResourceEntity::id) toDelete.map(NewsResourceEntity::id),
) )
assertEquals( assertEquals(
@ -177,26 +177,26 @@ class NewsResourceDaoTest {
.toSet(), .toSet(),
newsResourceDao.getNewsResources().first() newsResourceDao.getNewsResources().first()
.map { it.entity.id } .map { it.entity.id }
.toSet() .toSet(),
) )
} }
} }
private fun testTopicEntity( private fun testTopicEntity(
id: String = "0", id: String = "0",
name: String name: String,
) = TopicEntity( ) = TopicEntity(
id = id, id = id,
name = name, name = name,
shortDescription = "", shortDescription = "",
longDescription = "", longDescription = "",
url = "", url = "",
imageUrl = "" imageUrl = "",
) )
private fun testNewsResource( private fun testNewsResource(
id: String = "0", id: String = "0",
millisSinceEpoch: Long = 0 millisSinceEpoch: Long = 0,
) = NewsResourceEntity( ) = NewsResourceEntity(
id = id, id = id,
title = "", title = "",

@ -33,31 +33,31 @@ object DatabaseMigrations {
@RenameColumn( @RenameColumn(
tableName = "topics", tableName = "topics",
fromColumnName = "description", fromColumnName = "description",
toColumnName = "shortDescription" toColumnName = "shortDescription",
) )
class Schema2to3 : AutoMigrationSpec class Schema2to3 : AutoMigrationSpec
@DeleteColumn( @DeleteColumn(
tableName = "news_resources", tableName = "news_resources",
columnName = "episode_id" columnName = "episode_id",
) )
@DeleteTable.Entries( @DeleteTable.Entries(
DeleteTable( DeleteTable(
tableName = "episodes_authors" tableName = "episodes_authors",
), ),
DeleteTable( DeleteTable(
tableName = "episodes" tableName = "episodes",
) ),
) )
class Schema10to11 : AutoMigrationSpec class Schema10to11 : AutoMigrationSpec
@DeleteTable.Entries( @DeleteTable.Entries(
DeleteTable( DeleteTable(
tableName = "news_resources_authors" tableName = "news_resources_authors",
), ),
DeleteTable( DeleteTable(
tableName = "authors" tableName = "authors",
) ),
) )
class Schema11to12 : AutoMigrationSpec class Schema11to12 : AutoMigrationSpec
} }

@ -35,6 +35,6 @@ object DatabaseModule {
): NiaDatabase = Room.databaseBuilder( ): NiaDatabase = Room.databaseBuilder(
context, context,
NiaDatabase::class.java, NiaDatabase::class.java,
"nia-database" "nia-database",
).build() ).build()
} }

@ -46,7 +46,7 @@ import com.google.samples.apps.nowinandroid.core.database.util.NewsResourceTypeC
AutoMigration(from = 8, to = 9), AutoMigration(from = 8, to = 9),
AutoMigration(from = 9, to = 10), AutoMigration(from = 9, to = 10),
AutoMigration(from = 10, to = 11, spec = DatabaseMigrations.Schema10to11::class), AutoMigration(from = 10, to = 11, spec = DatabaseMigrations.Schema10to11::class),
AutoMigration(from = 11, to = 12, spec = DatabaseMigrations.Schema11to12::class) AutoMigration(from = 11, to = 12, spec = DatabaseMigrations.Schema11to12::class),
], ],
exportSchema = true, exportSchema = true,
) )

@ -39,7 +39,7 @@ interface NewsResourceDao {
value = """ value = """
SELECT * FROM news_resources SELECT * FROM news_resources
ORDER BY publish_date DESC ORDER BY publish_date DESC
""" """,
) )
fun getNewsResources(): Flow<List<PopulatedNewsResource>> fun getNewsResources(): Flow<List<PopulatedNewsResource>>
@ -53,7 +53,7 @@ interface NewsResourceDao {
WHERE topic_id IN (:filterTopicIds) WHERE topic_id IN (:filterTopicIds)
) )
ORDER BY publish_date DESC ORDER BY publish_date DESC
""" """,
) )
fun getNewsResources( fun getNewsResources(
filterTopicIds: Set<String> = emptySet(), filterTopicIds: Set<String> = emptySet(),
@ -79,7 +79,7 @@ interface NewsResourceDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnoreTopicCrossRefEntities( suspend fun insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences: List<NewsResourceTopicCrossRef> newsResourceTopicCrossReferences: List<NewsResourceTopicCrossRef>,
) )
/** /**
@ -89,7 +89,7 @@ interface NewsResourceDao {
value = """ value = """
DELETE FROM news_resources DELETE FROM news_resources
WHERE id in (:ids) WHERE id in (:ids)
""" """,
) )
suspend fun deleteNewsResources(ids: List<String>) suspend fun deleteNewsResources(ids: List<String>)
} }

@ -34,7 +34,7 @@ interface TopicDao {
value = """ value = """
SELECT * FROM topics SELECT * FROM topics
WHERE id = :topicId WHERE id = :topicId
""" """,
) )
fun getTopicEntity(topicId: String): Flow<TopicEntity> fun getTopicEntity(topicId: String): Flow<TopicEntity>
@ -45,7 +45,7 @@ interface TopicDao {
value = """ value = """
SELECT * FROM topics SELECT * FROM topics
WHERE id IN (:ids) WHERE id IN (:ids)
""" """,
) )
fun getTopicEntities(ids: Set<String>): Flow<List<TopicEntity>> fun getTopicEntities(ids: Set<String>): Flow<List<TopicEntity>>
@ -74,7 +74,7 @@ interface TopicDao {
value = """ value = """
DELETE FROM topics DELETE FROM topics
WHERE id in (:ids) WHERE id in (:ids)
""" """,
) )
suspend fun deleteTopics(ids: List<String>) suspend fun deleteTopics(ids: List<String>)
} }

@ -27,7 +27,7 @@ import kotlinx.datetime.Instant
* Defines an NiA news resource. * Defines an NiA news resource.
*/ */
@Entity( @Entity(
tableName = "news_resources" tableName = "news_resources",
) )
data class NewsResourceEntity( data class NewsResourceEntity(
@PrimaryKey @PrimaryKey
@ -50,5 +50,5 @@ fun NewsResourceEntity.asExternalModel() = NewsResource(
headerImageUrl = headerImageUrl, headerImageUrl = headerImageUrl,
publishDate = publishDate, publishDate = publishDate,
type = type, type = type,
topics = listOf() topics = listOf(),
) )

@ -32,13 +32,13 @@ import androidx.room.Index
entity = NewsResourceEntity::class, entity = NewsResourceEntity::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["news_resource_id"], childColumns = ["news_resource_id"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE,
), ),
ForeignKey( ForeignKey(
entity = TopicEntity::class, entity = TopicEntity::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["topic_id"], childColumns = ["topic_id"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE,
), ),
], ],
indices = [ indices = [

@ -34,9 +34,9 @@ data class PopulatedNewsResource(
value = NewsResourceTopicCrossRef::class, value = NewsResourceTopicCrossRef::class,
parentColumn = "news_resource_id", parentColumn = "news_resource_id",
entityColumn = "topic_id", entityColumn = "topic_id",
),
) )
) val topics: List<TopicEntity>,
val topics: List<TopicEntity>
) )
fun PopulatedNewsResource.asExternalModel() = NewsResource( fun PopulatedNewsResource.asExternalModel() = NewsResource(
@ -47,5 +47,5 @@ fun PopulatedNewsResource.asExternalModel() = NewsResource(
headerImageUrl = entity.headerImageUrl, headerImageUrl = entity.headerImageUrl,
publishDate = entity.publishDate, publishDate = entity.publishDate,
type = entity.type, type = entity.type,
topics = topics.map(TopicEntity::asExternalModel) topics = topics.map(TopicEntity::asExternalModel),
) )

@ -25,13 +25,13 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import javax.inject.Singleton
@Module @Module
@TestInstallIn( @TestInstallIn(
components = [SingletonComponent::class], components = [SingletonComponent::class],
replaces = [DataStoreModule::class] replaces = [DataStoreModule::class],
) )
object TestDataStoreModule { object TestDataStoreModule {
@ -39,13 +39,13 @@ object TestDataStoreModule {
@Singleton @Singleton
fun providesUserPreferencesDataStore( fun providesUserPreferencesDataStore(
userPreferencesSerializer: UserPreferencesSerializer, userPreferencesSerializer: UserPreferencesSerializer,
tmpFolder: TemporaryFolder tmpFolder: TemporaryFolder,
): DataStore<UserPreferences> = ): DataStore<UserPreferences> =
tmpFolder.testUserPreferencesDataStore(userPreferencesSerializer) tmpFolder.testUserPreferencesDataStore(userPreferencesSerializer)
} }
fun TemporaryFolder.testUserPreferencesDataStore( fun TemporaryFolder.testUserPreferencesDataStore(
userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer() userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer(),
) = DataStoreFactory.create( ) = DataStoreFactory.create(
serializer = userPreferencesSerializer, serializer = userPreferencesSerializer,
) { ) {

@ -30,14 +30,14 @@ object IntToStringIdsMigration : DataMigration<UserPreferences> {
// Migrate topic ids // Migrate topic ids
deprecatedFollowedTopicIds.clear() deprecatedFollowedTopicIds.clear()
deprecatedFollowedTopicIds.addAll( deprecatedFollowedTopicIds.addAll(
currentData.deprecatedIntFollowedTopicIdsList.map(Int::toString) currentData.deprecatedIntFollowedTopicIdsList.map(Int::toString),
) )
deprecatedIntFollowedTopicIds.clear() deprecatedIntFollowedTopicIds.clear()
// Migrate author ids // Migrate author ids
deprecatedFollowedAuthorIds.clear() deprecatedFollowedAuthorIds.clear()
deprecatedFollowedAuthorIds.addAll( deprecatedFollowedAuthorIds.addAll(
currentData.deprecatedIntFollowedAuthorIdsList.map(Int::toString) currentData.deprecatedIntFollowedAuthorIdsList.map(Int::toString),
) )
deprecatedIntFollowedAuthorIds.clear() deprecatedIntFollowedAuthorIds.clear()

@ -30,21 +30,21 @@ object ListToMapMigration : DataMigration<UserPreferences> {
// Migrate topic id lists // Migrate topic id lists
followedTopicIds.clear() followedTopicIds.clear()
followedTopicIds.putAll( followedTopicIds.putAll(
currentData.deprecatedFollowedTopicIdsList.associateWith { true } currentData.deprecatedFollowedTopicIdsList.associateWith { true },
) )
deprecatedFollowedTopicIds.clear() deprecatedFollowedTopicIds.clear()
// Migrate author ids // Migrate author ids
followedAuthorIds.clear() followedAuthorIds.clear()
followedAuthorIds.putAll( followedAuthorIds.putAll(
currentData.deprecatedFollowedAuthorIdsList.associateWith { true } currentData.deprecatedFollowedAuthorIdsList.associateWith { true },
) )
deprecatedFollowedAuthorIds.clear() deprecatedFollowedAuthorIds.clear()
// Migrate bookmarks // Migrate bookmarks
bookmarkedNewsResourceIds.clear() bookmarkedNewsResourceIds.clear()
bookmarkedNewsResourceIds.putAll( bookmarkedNewsResourceIds.putAll(
currentData.deprecatedBookmarkedNewsResourceIdsList.associateWith { true } currentData.deprecatedBookmarkedNewsResourceIdsList.associateWith { true },
) )
deprecatedBookmarkedNewsResourceIds.clear() deprecatedBookmarkedNewsResourceIds.clear()

@ -21,13 +21,13 @@ import androidx.datastore.core.DataStore
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig 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.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
import java.io.IOException
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.io.IOException
import javax.inject.Inject
class NiaPreferencesDataSource @Inject constructor( class NiaPreferencesDataSource @Inject constructor(
private val userPreferences: DataStore<UserPreferences> private val userPreferences: DataStore<UserPreferences>,
) { ) {
val userData = userPreferences.data val userData = userPreferences.data
.map { .map {
@ -38,21 +38,23 @@ class NiaPreferencesDataSource @Inject constructor(
null, null,
ThemeBrandProto.THEME_BRAND_UNSPECIFIED, ThemeBrandProto.THEME_BRAND_UNSPECIFIED,
ThemeBrandProto.UNRECOGNIZED, ThemeBrandProto.UNRECOGNIZED,
ThemeBrandProto.THEME_BRAND_DEFAULT -> ThemeBrand.DEFAULT ThemeBrandProto.THEME_BRAND_DEFAULT,
-> ThemeBrand.DEFAULT
ThemeBrandProto.THEME_BRAND_ANDROID -> ThemeBrand.ANDROID ThemeBrandProto.THEME_BRAND_ANDROID -> ThemeBrand.ANDROID
}, },
darkThemeConfig = when (it.darkThemeConfig) { darkThemeConfig = when (it.darkThemeConfig) {
null, null,
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED, DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
DarkThemeConfigProto.UNRECOGNIZED, DarkThemeConfigProto.UNRECOGNIZED,
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM -> DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM,
->
DarkThemeConfig.FOLLOW_SYSTEM DarkThemeConfig.FOLLOW_SYSTEM
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT ->
DarkThemeConfig.LIGHT DarkThemeConfig.LIGHT
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK
}, },
useDynamicColor = it.useDynamicColor, useDynamicColor = it.useDynamicColor,
shouldHideOnboarding = it.shouldHideOnboarding shouldHideOnboarding = it.shouldHideOnboarding,
) )
} }
@ -153,8 +155,8 @@ class NiaPreferencesDataSource @Inject constructor(
val updatedChangeListVersions = update( val updatedChangeListVersions = update(
ChangeListVersions( ChangeListVersions(
topicVersion = currentPreferences.topicChangeListVersion, topicVersion = currentPreferences.topicChangeListVersion,
newsResourceVersion = currentPreferences.newsResourceChangeListVersion newsResourceVersion = currentPreferences.newsResourceChangeListVersion,
) ),
) )
currentPreferences.copy { currentPreferences.copy {
@ -177,7 +179,6 @@ class NiaPreferencesDataSource @Inject constructor(
} }
private fun UserPreferencesKt.Dsl.updateShouldHideOnboardingIfNecessary() { private fun UserPreferencesKt.Dsl.updateShouldHideOnboardingIfNecessary() {
if (followedTopicIds.isEmpty() && followedAuthorIds.isEmpty()) { if (followedTopicIds.isEmpty() && followedAuthorIds.isEmpty()) {
shouldHideOnboarding = false shouldHideOnboarding = false
} }

@ -30,10 +30,10 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -44,14 +44,14 @@ object DataStoreModule {
fun providesUserPreferencesDataStore( fun providesUserPreferencesDataStore(
@ApplicationContext context: Context, @ApplicationContext context: Context,
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
userPreferencesSerializer: UserPreferencesSerializer userPreferencesSerializer: UserPreferencesSerializer,
): DataStore<UserPreferences> = ): DataStore<UserPreferences> =
DataStoreFactory.create( DataStoreFactory.create(
serializer = userPreferencesSerializer, serializer = userPreferencesSerializer,
scope = CoroutineScope(ioDispatcher + SupervisorJob()), scope = CoroutineScope(ioDispatcher + SupervisorJob()),
migrations = listOf( migrations = listOf(
IntToStringIdsMigration, IntToStringIdsMigration,
) ),
) { ) {
context.dataStoreFile("user_preferences.pb") context.dataStoreFile("user_preferences.pb")
} }

@ -16,10 +16,10 @@
package com.google.samples.apps.nowinandroid.core.datastore package com.google.samples.apps.nowinandroid.core.datastore
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/** /**
* Unit test for [IntToStringIdsMigration] * Unit test for [IntToStringIdsMigration]
@ -35,7 +35,7 @@ class IntToStringIdsMigrationTest {
// Assert that there are no string topic ids yet // Assert that there are no string topic ids yet
assertEquals( assertEquals(
emptyList<String>(), emptyList<String>(),
preMigrationUserPreferences.deprecatedFollowedTopicIdsList preMigrationUserPreferences.deprecatedFollowedTopicIdsList,
) )
// Run the migration // Run the migration
@ -48,7 +48,7 @@ class IntToStringIdsMigrationTest {
deprecatedFollowedTopicIds.addAll(listOf("1", "2", "3")) deprecatedFollowedTopicIds.addAll(listOf("1", "2", "3"))
hasDoneIntToStringIdMigration = true hasDoneIntToStringIdMigration = true
}, },
postMigrationUserPreferences postMigrationUserPreferences,
) )
// Assert that the migration has been marked complete // Assert that the migration has been marked complete
@ -64,7 +64,7 @@ class IntToStringIdsMigrationTest {
// Assert that there are no string author ids yet // Assert that there are no string author ids yet
assertEquals( assertEquals(
emptyList<String>(), emptyList<String>(),
preMigrationUserPreferences.deprecatedFollowedAuthorIdsList preMigrationUserPreferences.deprecatedFollowedAuthorIdsList,
) )
// Run the migration // Run the migration
@ -77,7 +77,7 @@ class IntToStringIdsMigrationTest {
deprecatedFollowedAuthorIds.addAll(listOf("4", "5", "6")) deprecatedFollowedAuthorIds.addAll(listOf("4", "5", "6"))
hasDoneIntToStringIdMigration = true hasDoneIntToStringIdMigration = true
}, },
postMigrationUserPreferences postMigrationUserPreferences,
) )
// Assert that the migration has been marked complete // Assert that the migration has been marked complete

@ -16,10 +16,10 @@
package com.google.samples.apps.nowinandroid.core.datastore package com.google.samples.apps.nowinandroid.core.datastore
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ListToMapMigrationTest { class ListToMapMigrationTest {
@ -32,7 +32,7 @@ class ListToMapMigrationTest {
// Assert that there are no topic ids in the map yet // Assert that there are no topic ids in the map yet
assertEquals( assertEquals(
emptyMap<String, Boolean>(), emptyMap<String, Boolean>(),
preMigrationUserPreferences.followedTopicIdsMap preMigrationUserPreferences.followedTopicIdsMap,
) )
// Run the migration // Run the migration
@ -42,7 +42,7 @@ class ListToMapMigrationTest {
// Assert the deprecated topic ids have been migrated to the topic ids map // Assert the deprecated topic ids have been migrated to the topic ids map
assertEquals( assertEquals(
mapOf("1" to true, "2" to true, "3" to true), mapOf("1" to true, "2" to true, "3" to true),
postMigrationUserPreferences.followedTopicIdsMap postMigrationUserPreferences.followedTopicIdsMap,
) )
// Assert that the migration has been marked complete // Assert that the migration has been marked complete
@ -58,7 +58,7 @@ class ListToMapMigrationTest {
// Assert that there are no author ids in the map yet // Assert that there are no author ids in the map yet
assertEquals( assertEquals(
emptyMap<String, Boolean>(), emptyMap<String, Boolean>(),
preMigrationUserPreferences.followedAuthorIdsMap preMigrationUserPreferences.followedAuthorIdsMap,
) )
// Run the migration // Run the migration
@ -68,7 +68,7 @@ class ListToMapMigrationTest {
// Assert the deprecated author ids have been migrated to the author ids map // Assert the deprecated author ids have been migrated to the author ids map
assertEquals( assertEquals(
mapOf("4" to true, "5" to true, "6" to true), mapOf("4" to true, "5" to true, "6" to true),
postMigrationUserPreferences.followedAuthorIdsMap postMigrationUserPreferences.followedAuthorIdsMap,
) )
// Assert that the migration has been marked complete // Assert that the migration has been marked complete
@ -84,7 +84,7 @@ class ListToMapMigrationTest {
// Assert that there are no bookmarks in the map yet // Assert that there are no bookmarks in the map yet
assertEquals( assertEquals(
emptyMap<String, Boolean>(), emptyMap<String, Boolean>(),
preMigrationUserPreferences.bookmarkedNewsResourceIdsMap preMigrationUserPreferences.bookmarkedNewsResourceIdsMap,
) )
// Run the migration // Run the migration
@ -94,7 +94,7 @@ class ListToMapMigrationTest {
// Assert the deprecated bookmarks have been migrated to the bookmarks map // Assert the deprecated bookmarks have been migrated to the bookmarks map
assertEquals( assertEquals(
mapOf("7" to true, "8" to true, "9" to true), mapOf("7" to true, "8" to true, "9" to true),
postMigrationUserPreferences.bookmarkedNewsResourceIdsMap postMigrationUserPreferences.bookmarkedNewsResourceIdsMap,
) )
// Assert that the migration has been marked complete // Assert that the migration has been marked complete

@ -17,14 +17,14 @@
package com.google.samples.apps.nowinandroid.core.datastore package com.google.samples.apps.nowinandroid.core.datastore
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class NiaPreferencesDataSourceTest { class NiaPreferencesDataSourceTest {
private lateinit var subject: NiaPreferencesDataSource private lateinit var subject: NiaPreferencesDataSource
@ -35,7 +35,7 @@ class NiaPreferencesDataSourceTest {
@Before @Before
fun setup() { fun setup() {
subject = NiaPreferencesDataSource( subject = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore() tmpFolder.testUserPreferencesDataStore(),
) )
} }
@ -52,7 +52,6 @@ class NiaPreferencesDataSourceTest {
@Test @Test
fun userShouldHideOnboarding_unfollowsLastTopic_shouldHideOnboardingIsFalse() = runTest { fun userShouldHideOnboarding_unfollowsLastTopic_shouldHideOnboardingIsFalse() = runTest {
// Given: user completes onboarding by selecting a single topic. // Given: user completes onboarding by selecting a single topic.
subject.toggleFollowedTopicId("1", true) subject.toggleFollowedTopicId("1", true)
subject.setShouldHideOnboarding(true) subject.setShouldHideOnboarding(true)
@ -66,7 +65,6 @@ class NiaPreferencesDataSourceTest {
@Test @Test
fun userShouldHideOnboarding_unfollowsAllTopics_shouldHideOnboardingIsFalse() = runTest { fun userShouldHideOnboarding_unfollowsAllTopics_shouldHideOnboardingIsFalse() = runTest {
// Given: user completes onboarding by selecting several topics. // Given: user completes onboarding by selecting several topics.
subject.setFollowedTopicIds(setOf("1", "2")) subject.setFollowedTopicIds(setOf("1", "2"))
subject.setShouldHideOnboarding(true) subject.setShouldHideOnboarding(true)

@ -17,11 +17,11 @@
package com.google.samples.apps.nowinandroid.core.datastore package com.google.samples.apps.nowinandroid.core.datastore
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
class UserPreferencesSerializerTest { class UserPreferencesSerializerTest {
private val userPreferencesSerializer = UserPreferencesSerializer() private val userPreferencesSerializer = UserPreferencesSerializer()
@ -32,7 +32,7 @@ class UserPreferencesSerializerTest {
userPreferences { userPreferences {
// Default value // Default value
}, },
userPreferencesSerializer.defaultValue userPreferencesSerializer.defaultValue,
) )
} }
@ -53,7 +53,7 @@ class UserPreferencesSerializerTest {
assertEquals( assertEquals(
expectedUserPreferences, expectedUserPreferences,
actualUserPreferences actualUserPreferences,
) )
} }

@ -41,9 +41,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradien
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme
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.designsystem.theme.TintTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.TintTheme
import kotlin.test.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
/** /**
* Tests [NiaTheme] using different combinations of the theme mode parameters: * Tests [NiaTheme] using different combinations of the theme mode parameters:
@ -64,7 +64,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = false, darkTheme = false,
disableDynamicTheming = true, disableDynamicTheming = true,
androidTheme = false androidTheme = false,
) { ) {
val colorScheme = LightDefaultColorScheme val colorScheme = LightDefaultColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -84,7 +84,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = true, darkTheme = true,
disableDynamicTheming = true, disableDynamicTheming = true,
androidTheme = false androidTheme = false,
) { ) {
val colorScheme = DarkDefaultColorScheme val colorScheme = DarkDefaultColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -104,7 +104,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = false, darkTheme = false,
disableDynamicTheming = false, disableDynamicTheming = false,
androidTheme = false androidTheme = false,
) { ) {
val colorScheme = dynamicLightColorSchemeWithFallback() val colorScheme = dynamicLightColorSchemeWithFallback()
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -124,7 +124,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = true, darkTheme = true,
disableDynamicTheming = false, disableDynamicTheming = false,
androidTheme = false androidTheme = false,
) { ) {
val colorScheme = dynamicDarkColorSchemeWithFallback() val colorScheme = dynamicDarkColorSchemeWithFallback()
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -144,7 +144,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = false, darkTheme = false,
disableDynamicTheming = true, disableDynamicTheming = true,
androidTheme = true androidTheme = true,
) { ) {
val colorScheme = LightAndroidColorScheme val colorScheme = LightAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -164,7 +164,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = true, darkTheme = true,
disableDynamicTheming = true, disableDynamicTheming = true,
androidTheme = true androidTheme = true,
) { ) {
val colorScheme = DarkAndroidColorScheme val colorScheme = DarkAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -184,7 +184,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = false, darkTheme = false,
disableDynamicTheming = false, disableDynamicTheming = false,
androidTheme = true androidTheme = true,
) { ) {
val colorScheme = LightAndroidColorScheme val colorScheme = LightAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -204,7 +204,7 @@ class ThemeTest {
NiaTheme( NiaTheme(
darkTheme = true, darkTheme = true,
disableDynamicTheming = false, disableDynamicTheming = false,
androidTheme = true androidTheme = true,
) { ) {
val colorScheme = DarkAndroidColorScheme val colorScheme = DarkAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -244,7 +244,7 @@ class ThemeTest {
return GradientColors( return GradientColors(
top = colorScheme.inverseOnSurface, top = colorScheme.inverseOnSurface,
bottom = colorScheme.primaryContainer, bottom = colorScheme.primaryContainer,
container = colorScheme.surface container = colorScheme.surface,
) )
} }
@ -259,7 +259,7 @@ class ThemeTest {
private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme { private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme {
return BackgroundTheme( return BackgroundTheme(
color = colorScheme.surface, color = colorScheme.surface,
tonalElevation = 2.dp tonalElevation = 2.dp,
) )
} }
@ -280,7 +280,7 @@ class ThemeTest {
*/ */
private fun assertColorSchemesEqual( private fun assertColorSchemesEqual(
expectedColorScheme: ColorScheme, expectedColorScheme: ColorScheme,
actualColorScheme: ColorScheme actualColorScheme: ColorScheme,
) { ) {
assertEquals(expectedColorScheme.primary, actualColorScheme.primary) assertEquals(expectedColorScheme.primary, actualColorScheme.primary)
assertEquals(expectedColorScheme.onPrimary, actualColorScheme.onPrimary) assertEquals(expectedColorScheme.onPrimary, actualColorScheme.onPrimary)
@ -291,7 +291,7 @@ class ThemeTest {
assertEquals(expectedColorScheme.secondaryContainer, actualColorScheme.secondaryContainer) assertEquals(expectedColorScheme.secondaryContainer, actualColorScheme.secondaryContainer)
assertEquals( assertEquals(
expectedColorScheme.onSecondaryContainer, expectedColorScheme.onSecondaryContainer,
actualColorScheme.onSecondaryContainer actualColorScheme.onSecondaryContainer,
) )
assertEquals(expectedColorScheme.tertiary, actualColorScheme.tertiary) assertEquals(expectedColorScheme.tertiary, actualColorScheme.tertiary)
assertEquals(expectedColorScheme.onTertiary, actualColorScheme.onTertiary) assertEquals(expectedColorScheme.onTertiary, actualColorScheme.onTertiary)

@ -50,7 +50,7 @@ import kotlin.math.tan
@Composable @Composable
fun NiaBackground( fun NiaBackground(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable () -> Unit content: @Composable () -> Unit,
) { ) {
val color = LocalBackgroundTheme.current.color val color = LocalBackgroundTheme.current.color
val tonalElevation = LocalBackgroundTheme.current.tonalElevation val tonalElevation = LocalBackgroundTheme.current.tonalElevation
@ -77,7 +77,7 @@ fun NiaBackground(
fun NiaGradientBackground( fun NiaGradientBackground(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
gradientColors: GradientColors = LocalGradientColors.current, gradientColors: GradientColors = LocalGradientColors.current,
content: @Composable () -> Unit content: @Composable () -> Unit,
) { ) {
val currentTopColor by rememberUpdatedState(gradientColors.top) val currentTopColor by rememberUpdatedState(gradientColors.top)
val currentBottomColor by rememberUpdatedState(gradientColors.bottom) val currentBottomColor by rememberUpdatedState(gradientColors.bottom)
@ -87,7 +87,7 @@ fun NiaGradientBackground(
} else { } else {
gradientColors.container gradientColors.container
}, },
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize(),
) { ) {
Box( Box(
Modifier Modifier
@ -98,7 +98,7 @@ fun NiaGradientBackground(
val offset = size.height * tan( val offset = size.height * tan(
Math Math
.toRadians(11.06) .toRadians(11.06)
.toFloat() .toFloat(),
) )
val start = Offset(size.width / 2 + offset / 2, 0f) val start = Offset(size.width / 2 + offset / 2, 0f)
@ -132,7 +132,7 @@ fun NiaGradientBackground(
drawRect(topGradient) drawRect(topGradient)
drawRect(bottomGradient) drawRect(bottomGradient)
} }
} },
) { ) {
content() content()
} }

@ -48,17 +48,17 @@ fun NiaButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding, contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit,
) { ) {
Button( Button(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.onBackground containerColor = MaterialTheme.colorScheme.onBackground,
), ),
contentPadding = contentPadding, contentPadding = contentPadding,
content = content content = content,
) )
} }
@ -78,7 +78,7 @@ fun NiaButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
text: @Composable () -> Unit, text: @Composable () -> Unit,
leadingIcon: @Composable (() -> Unit)? = null leadingIcon: @Composable (() -> Unit)? = null,
) { ) {
NiaButton( NiaButton(
onClick = onClick, onClick = onClick,
@ -88,11 +88,11 @@ fun NiaButton(
ButtonDefaults.ButtonWithIconContentPadding ButtonDefaults.ButtonWithIconContentPadding
} else { } else {
ButtonDefaults.ContentPadding ButtonDefaults.ContentPadding
} },
) { ) {
NiaButtonContent( NiaButtonContent(
text = text, text = text,
leadingIcon = leadingIcon leadingIcon = leadingIcon,
) )
} }
} }
@ -114,14 +114,14 @@ fun NiaOutlinedButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding, contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit,
) { ) {
OutlinedButton( OutlinedButton(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground contentColor = MaterialTheme.colorScheme.onBackground,
), ),
border = BorderStroke( border = BorderStroke(
width = NiaButtonDefaults.OutlinedButtonBorderWidth, width = NiaButtonDefaults.OutlinedButtonBorderWidth,
@ -129,12 +129,12 @@ fun NiaOutlinedButton(
MaterialTheme.colorScheme.outline MaterialTheme.colorScheme.outline
} else { } else {
MaterialTheme.colorScheme.onSurface.copy( MaterialTheme.colorScheme.onSurface.copy(
alpha = NiaButtonDefaults.DisabledOutlinedButtonBorderAlpha alpha = NiaButtonDefaults.DisabledOutlinedButtonBorderAlpha,
) )
} },
), ),
contentPadding = contentPadding, contentPadding = contentPadding,
content = content content = content,
) )
} }
@ -154,7 +154,7 @@ fun NiaOutlinedButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
text: @Composable () -> Unit, text: @Composable () -> Unit,
leadingIcon: @Composable (() -> Unit)? = null leadingIcon: @Composable (() -> Unit)? = null,
) { ) {
NiaOutlinedButton( NiaOutlinedButton(
onClick = onClick, onClick = onClick,
@ -164,11 +164,11 @@ fun NiaOutlinedButton(
ButtonDefaults.ButtonWithIconContentPadding ButtonDefaults.ButtonWithIconContentPadding
} else { } else {
ButtonDefaults.ContentPadding ButtonDefaults.ContentPadding
} },
) { ) {
NiaButtonContent( NiaButtonContent(
text = text, text = text,
leadingIcon = leadingIcon leadingIcon = leadingIcon,
) )
} }
} }
@ -187,16 +187,16 @@ fun NiaTextButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit,
) { ) {
TextButton( TextButton(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground contentColor = MaterialTheme.colorScheme.onBackground,
), ),
content = content content = content,
) )
} }
@ -216,16 +216,16 @@ fun NiaTextButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
text: @Composable () -> Unit, text: @Composable () -> Unit,
leadingIcon: @Composable (() -> Unit)? = null leadingIcon: @Composable (() -> Unit)? = null,
) { ) {
NiaTextButton( NiaTextButton(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled enabled = enabled,
) { ) {
NiaButtonContent( NiaButtonContent(
text = text, text = text,
leadingIcon = leadingIcon leadingIcon = leadingIcon,
) )
} }
} }
@ -239,7 +239,7 @@ fun NiaTextButton(
@Composable @Composable
private fun NiaButtonContent( private fun NiaButtonContent(
text: @Composable () -> Unit, text: @Composable () -> Unit,
leadingIcon: @Composable (() -> Unit)? = null leadingIcon: @Composable (() -> Unit)? = null,
) { ) {
if (leadingIcon != null) { if (leadingIcon != null) {
Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) { Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) {
@ -253,8 +253,8 @@ private fun NiaButtonContent(
ButtonDefaults.IconSpacing ButtonDefaults.IconSpacing
} else { } else {
0.dp 0.dp
} },
) ),
) { ) {
text() text()
} }
@ -267,6 +267,7 @@ object NiaButtonDefaults {
// TODO: File bug // TODO: File bug
// OutlinedButton border color doesn't respect disabled state by default // OutlinedButton border color doesn't respect disabled state by default
const val DisabledOutlinedButtonBorderAlpha = 0.12f const val DisabledOutlinedButtonBorderAlpha = 0.12f
// TODO: File bug // TODO: File bug
// OutlinedButton default border width isn't exposed via ButtonDefaults // OutlinedButton default border width isn't exposed via ButtonDefaults
val OutlinedButtonBorderWidth = 1.dp val OutlinedButtonBorderWidth = 1.dp

@ -46,7 +46,7 @@ fun NiaFilterChip(
onSelectedChange: (Boolean) -> Unit, onSelectedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
label: @Composable () -> Unit label: @Composable () -> Unit,
) { ) {
FilterChip( FilterChip(
selected = selected, selected = selected,
@ -62,7 +62,7 @@ fun NiaFilterChip(
{ {
Icon( Icon(
imageVector = NiaIcons.Check, imageVector = NiaIcons.Check,
contentDescription = null contentDescription = null,
) )
} }
} else { } else {
@ -73,33 +73,33 @@ fun NiaFilterChip(
borderColor = MaterialTheme.colorScheme.onBackground, borderColor = MaterialTheme.colorScheme.onBackground,
selectedBorderColor = MaterialTheme.colorScheme.onBackground, selectedBorderColor = MaterialTheme.colorScheme.onBackground,
disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy( disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha alpha = NiaChipDefaults.DisabledChipContentAlpha,
), ),
disabledSelectedBorderColor = MaterialTheme.colorScheme.onBackground.copy( disabledSelectedBorderColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha alpha = NiaChipDefaults.DisabledChipContentAlpha,
), ),
selectedBorderWidth = NiaChipDefaults.ChipBorderWidth selectedBorderWidth = NiaChipDefaults.ChipBorderWidth,
), ),
colors = FilterChipDefaults.filterChipColors( colors = FilterChipDefaults.filterChipColors(
labelColor = MaterialTheme.colorScheme.onBackground, labelColor = MaterialTheme.colorScheme.onBackground,
iconColor = MaterialTheme.colorScheme.onBackground, iconColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = if (selected) { disabledContainerColor = if (selected) {
MaterialTheme.colorScheme.onBackground.copy( MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContainerAlpha alpha = NiaChipDefaults.DisabledChipContainerAlpha,
) )
} else { } else {
Color.Transparent Color.Transparent
}, },
disabledLabelColor = MaterialTheme.colorScheme.onBackground.copy( disabledLabelColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha alpha = NiaChipDefaults.DisabledChipContentAlpha,
), ),
disabledLeadingIconColor = MaterialTheme.colorScheme.onBackground.copy( disabledLeadingIconColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha alpha = NiaChipDefaults.DisabledChipContentAlpha,
), ),
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onBackground, selectedLabelColor = MaterialTheme.colorScheme.onBackground,
selectedLeadingIconColor = MaterialTheme.colorScheme.onBackground selectedLeadingIconColor = MaterialTheme.colorScheme.onBackground,
) ),
) )
} }

@ -61,7 +61,7 @@ fun <T> NiaDropdownMenuButton(
text: @Composable () -> Unit, text: @Composable () -> Unit,
itemText: @Composable (item: T) -> Unit, itemText: @Composable (item: T) -> Unit,
itemLeadingIcon: @Composable ((item: T) -> Unit)? = null, itemLeadingIcon: @Composable ((item: T) -> Unit)? = null,
itemTrailingIcon: @Composable ((item: T) -> Unit)? = null itemTrailingIcon: @Composable ((item: T) -> Unit)? = null,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box(modifier = modifier) { Box(modifier = modifier) {
@ -69,7 +69,7 @@ fun <T> NiaDropdownMenuButton(
onClick = { expanded = true }, onClick = { expanded = true },
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground contentColor = MaterialTheme.colorScheme.onBackground,
), ),
border = BorderStroke( border = BorderStroke(
width = NiaDropdownMenuDefaults.DropdownMenuButtonBorderWidth, width = NiaDropdownMenuDefaults.DropdownMenuButtonBorderWidth,
@ -77,11 +77,11 @@ fun <T> NiaDropdownMenuButton(
MaterialTheme.colorScheme.outline MaterialTheme.colorScheme.outline
} else { } else {
MaterialTheme.colorScheme.onSurface.copy( MaterialTheme.colorScheme.onSurface.copy(
alpha = NiaDropdownMenuDefaults.DisabledDropdownMenuButtonBorderAlpha alpha = NiaDropdownMenuDefaults.DisabledDropdownMenuButtonBorderAlpha,
) )
} },
), ),
contentPadding = NiaDropdownMenuDefaults.DropdownMenuButtonContentPadding contentPadding = NiaDropdownMenuDefaults.DropdownMenuButtonContentPadding,
) { ) {
NiaDropdownMenuButtonContent( NiaDropdownMenuButtonContent(
text = text, text = text,
@ -92,9 +92,9 @@ fun <T> NiaDropdownMenuButton(
} else { } else {
NiaIcons.ArrowDropDown NiaIcons.ArrowDropDown
}, },
contentDescription = null contentDescription = null,
) )
} },
) )
} }
NiaDropdownMenu( NiaDropdownMenu(
@ -105,7 +105,7 @@ fun <T> NiaDropdownMenuButton(
dismissOnItemClick = dismissOnItemClick, dismissOnItemClick = dismissOnItemClick,
itemText = itemText, itemText = itemText,
itemLeadingIcon = itemLeadingIcon, itemLeadingIcon = itemLeadingIcon,
itemTrailingIcon = itemTrailingIcon itemTrailingIcon = itemTrailingIcon,
) )
} }
} }
@ -129,8 +129,8 @@ private fun NiaDropdownMenuButtonContent(
ButtonDefaults.IconSpacing ButtonDefaults.IconSpacing
} else { } else {
0.dp 0.dp
} },
) ),
) { ) {
ProvideTextStyle(value = MaterialTheme.typography.labelSmall) { ProvideTextStyle(value = MaterialTheme.typography.labelSmall) {
text() text()
@ -166,11 +166,11 @@ fun <T> NiaDropdownMenu(
dismissOnItemClick: Boolean = true, dismissOnItemClick: Boolean = true,
itemText: @Composable (item: T) -> Unit, itemText: @Composable (item: T) -> Unit,
itemLeadingIcon: @Composable ((item: T) -> Unit)? = null, itemLeadingIcon: @Composable ((item: T) -> Unit)? = null,
itemTrailingIcon: @Composable ((item: T) -> Unit)? = null itemTrailingIcon: @Composable ((item: T) -> Unit)? = null,
) { ) {
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = onDismissRequest onDismissRequest = onDismissRequest,
) { ) {
items.forEach { item -> items.forEach { item ->
DropdownMenuItem( DropdownMenuItem(
@ -188,7 +188,7 @@ fun <T> NiaDropdownMenu(
{ itemTrailingIcon(item) } { itemTrailingIcon(item) }
} else { } else {
null null
} },
) )
} }
} }
@ -201,9 +201,11 @@ object NiaDropdownMenuDefaults {
// TODO: File bug // TODO: File bug
// OutlinedButton border color doesn't respect disabled state by default // OutlinedButton border color doesn't respect disabled state by default
const val DisabledDropdownMenuButtonBorderAlpha = 0.12f const val DisabledDropdownMenuButtonBorderAlpha = 0.12f
// TODO: File bug // TODO: File bug
// OutlinedButton default border width isn't exposed via ButtonDefaults // OutlinedButton default border width isn't exposed via ButtonDefaults
val DropdownMenuButtonBorderWidth = 1.dp val DropdownMenuButtonBorderWidth = 1.dp
// TODO: File bug // TODO: File bug
// Various default button padding values aren't exposed via ButtonDefaults // Various default button padding values aren't exposed via ButtonDefaults
val DropdownMenuButtonContentPadding = val DropdownMenuButtonContentPadding =
@ -211,6 +213,6 @@ object NiaDropdownMenuDefaults {
start = 24.dp, start = 24.dp,
top = 8.dp, top = 8.dp,
end = 16.dp, end = 16.dp,
bottom = 8.dp bottom = 8.dp,
) )
} }

@ -31,7 +31,7 @@ fun DynamicAsyncImage(
imageUrl: String, imageUrl: String,
contentDescription: String?, contentDescription: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
placeholder: Painter? = null placeholder: Painter? = null,
) { ) {
val iconTint = LocalTintTheme.current.iconTint val iconTint = LocalTintTheme.current.iconTint
AsyncImage( AsyncImage(
@ -39,6 +39,6 @@ fun DynamicAsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = contentDescription, contentDescription = contentDescription,
colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null,
modifier = modifier modifier = modifier,
) )
} }

@ -43,7 +43,7 @@ fun NiaIconToggleButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
icon: @Composable () -> Unit, icon: @Composable () -> Unit,
checkedIcon: @Composable () -> Unit = icon checkedIcon: @Composable () -> Unit = icon,
) { ) {
// TODO: File bug // TODO: File bug
// Can't use regular IconToggleButton as it doesn't include a shape (appears square) // Can't use regular IconToggleButton as it doesn't include a shape (appears square)
@ -57,12 +57,12 @@ fun NiaIconToggleButton(
checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer, checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
disabledContainerColor = if (checked) { disabledContainerColor = if (checked) {
MaterialTheme.colorScheme.onBackground.copy( MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaIconButtonDefaults.DisabledIconButtonContainerAlpha alpha = NiaIconButtonDefaults.DisabledIconButtonContainerAlpha,
) )
} else { } else {
Color.Transparent Color.Transparent
} },
) ),
) { ) {
if (checked) checkedIcon() else icon() if (checked) checkedIcon() else icon()
} }

@ -53,7 +53,7 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun NiaLoadingWheel( fun NiaLoadingWheel(
contentDesc: String, contentDesc: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
val infiniteTransition = rememberInfiniteTransition() val infiniteTransition = rememberInfiniteTransition()
@ -68,8 +68,8 @@ fun NiaLoadingWheel(
animationSpec = tween( animationSpec = tween(
durationMillis = 100, durationMillis = 100,
easing = FastOutSlowInEasing, easing = FastOutSlowInEasing,
delayMillis = 40 * index delayMillis = 40 * index,
) ),
) )
} }
} }
@ -80,8 +80,8 @@ fun NiaLoadingWheel(
initialValue = 0F, initialValue = 0F,
targetValue = 360F, targetValue = 360F,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(durationMillis = ROTATION_TIME, easing = LinearEasing) animation = tween(durationMillis = ROTATION_TIME, easing = LinearEasing),
) ),
) )
// Specifies the color animation for the base-to-progress line color change // Specifies the color animation for the base-to-progress line color change
@ -98,8 +98,8 @@ fun NiaLoadingWheel(
baseLineColor at ROTATION_TIME / NUM_OF_LINES with LinearEasing baseLineColor at ROTATION_TIME / NUM_OF_LINES with LinearEasing
}, },
repeatMode = RepeatMode.Restart, repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(ROTATION_TIME / NUM_OF_LINES / 2 * index) initialStartOffset = StartOffset(ROTATION_TIME / NUM_OF_LINES / 2 * index),
) ),
) )
} }
@ -121,7 +121,7 @@ fun NiaLoadingWheel(
strokeWidth = 4F, strokeWidth = 4F,
cap = StrokeCap.Round, cap = StrokeCap.Round,
start = Offset(size.width / 2, size.height / 4), start = Offset(size.width / 2, size.height / 4),
end = Offset(size.width / 2, floatAnimValues[index].value * size.height / 4) end = Offset(size.width / 2, floatAnimValues[index].value * size.height / 4),
) )
} }
} }
@ -131,7 +131,7 @@ fun NiaLoadingWheel(
@Composable @Composable
fun NiaOverlayLoadingWheel( fun NiaOverlayLoadingWheel(
contentDesc: String, contentDesc: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
Surface( Surface(
shape = RoundedCornerShape(60.dp), shape = RoundedCornerShape(60.dp),

@ -54,7 +54,7 @@ fun RowScope.NiaNavigationBarItem(
selectedIcon: @Composable () -> Unit = icon, selectedIcon: @Composable () -> Unit = icon,
enabled: Boolean = true, enabled: Boolean = true,
label: @Composable (() -> Unit)? = null, label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true alwaysShowLabel: Boolean = true,
) { ) {
NavigationBarItem( NavigationBarItem(
selected = selected, selected = selected,
@ -69,8 +69,8 @@ fun RowScope.NiaNavigationBarItem(
unselectedIconColor = NiaNavigationDefaults.navigationContentColor(), unselectedIconColor = NiaNavigationDefaults.navigationContentColor(),
selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(), selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(),
unselectedTextColor = NiaNavigationDefaults.navigationContentColor(), unselectedTextColor = NiaNavigationDefaults.navigationContentColor(),
indicatorColor = NiaNavigationDefaults.navigationIndicatorColor() indicatorColor = NiaNavigationDefaults.navigationIndicatorColor(),
) ),
) )
} }
@ -84,13 +84,13 @@ fun RowScope.NiaNavigationBarItem(
@Composable @Composable
fun NiaNavigationBar( fun NiaNavigationBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit,
) { ) {
NavigationBar( NavigationBar(
modifier = modifier, modifier = modifier,
contentColor = NiaNavigationDefaults.navigationContentColor(), contentColor = NiaNavigationDefaults.navigationContentColor(),
tonalElevation = 0.dp, tonalElevation = 0.dp,
content = content content = content,
) )
} }
@ -118,7 +118,7 @@ fun NiaNavigationRailItem(
selectedIcon: @Composable () -> Unit = icon, selectedIcon: @Composable () -> Unit = icon,
enabled: Boolean = true, enabled: Boolean = true,
label: @Composable (() -> Unit)? = null, label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true alwaysShowLabel: Boolean = true,
) { ) {
NavigationRailItem( NavigationRailItem(
selected = selected, selected = selected,
@ -133,8 +133,8 @@ fun NiaNavigationRailItem(
unselectedIconColor = NiaNavigationDefaults.navigationContentColor(), unselectedIconColor = NiaNavigationDefaults.navigationContentColor(),
selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(), selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(),
unselectedTextColor = NiaNavigationDefaults.navigationContentColor(), unselectedTextColor = NiaNavigationDefaults.navigationContentColor(),
indicatorColor = NiaNavigationDefaults.navigationIndicatorColor() indicatorColor = NiaNavigationDefaults.navigationIndicatorColor(),
) ),
) )
} }
@ -150,14 +150,14 @@ fun NiaNavigationRailItem(
fun NiaNavigationRail( fun NiaNavigationRail(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
header: @Composable (ColumnScope.() -> Unit)? = null, header: @Composable (ColumnScope.() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit,
) { ) {
NavigationRail( NavigationRail(
modifier = modifier, modifier = modifier,
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentColor = NiaNavigationDefaults.navigationContentColor(), contentColor = NiaNavigationDefaults.navigationContentColor(),
header = header, header = header,
content = content content = content,
) )
} }
@ -167,8 +167,10 @@ fun NiaNavigationRail(
object NiaNavigationDefaults { object NiaNavigationDefaults {
@Composable @Composable
fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant
@Composable @Composable
fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer
@Composable @Composable
fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer
} }

@ -46,7 +46,7 @@ fun NiaTab(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
text: @Composable () -> Unit text: @Composable () -> Unit,
) { ) {
Tab( Tab(
selected = selected, selected = selected,
@ -61,9 +61,9 @@ fun NiaTab(
Box(modifier = Modifier.padding(top = NiaTabDefaults.TabTopPadding)) { Box(modifier = Modifier.padding(top = NiaTabDefaults.TabTopPadding)) {
text() text()
} }
} },
) )
} },
) )
} }
@ -79,7 +79,7 @@ fun NiaTab(
fun NiaTabRow( fun NiaTabRow(
selectedTabIndex: Int, selectedTabIndex: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tabs: @Composable () -> Unit tabs: @Composable () -> Unit,
) { ) {
TabRow( TabRow(
selectedTabIndex = selectedTabIndex, selectedTabIndex = selectedTabIndex,
@ -90,10 +90,10 @@ fun NiaTabRow(
TabRowDefaults.Indicator( TabRowDefaults.Indicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]), modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
height = 2.dp, height = 2.dp,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
}, },
tabs = tabs tabs = tabs,
) )
} }

@ -41,15 +41,14 @@ fun NiaTopicTag(
text: @Composable () -> Unit, text: @Composable () -> Unit,
followText: @Composable () -> Unit = { Text(stringResource(R.string.follow)) }, followText: @Composable () -> Unit = { Text(stringResource(R.string.follow)) },
unFollowText: @Composable () -> Unit = { Text(stringResource(R.string.unfollow)) }, unFollowText: @Composable () -> Unit = { Text(stringResource(R.string.unfollow)) },
browseText: @Composable () -> Unit = { Text(stringResource(R.string.browse_topic)) } browseText: @Composable () -> Unit = { Text(stringResource(R.string.browse_topic)) },
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
val containerColor = if (followed) { val containerColor = if (followed) {
MaterialTheme.colorScheme.primaryContainer MaterialTheme.colorScheme.primaryContainer
} else { } else {
MaterialTheme.colorScheme.surfaceVariant.copy( MaterialTheme.colorScheme.surfaceVariant.copy(
alpha = NiaTagDefaults.UnfollowedTopicTagContainerAlpha alpha = NiaTagDefaults.UnfollowedTopicTagContainerAlpha,
) )
} }
TextButton( TextButton(
@ -59,9 +58,9 @@ fun NiaTopicTag(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColorFor(backgroundColor = containerColor), contentColor = contentColorFor(backgroundColor = containerColor),
disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy( disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy(
alpha = NiaTagDefaults.DisabledTopicTagContainerAlpha alpha = NiaTagDefaults.DisabledTopicTagContainerAlpha,
) ),
) ),
) { ) {
ProvideTextStyle(value = MaterialTheme.typography.labelSmall) { ProvideTextStyle(value = MaterialTheme.typography.labelSmall) {
text() text()
@ -84,7 +83,7 @@ fun NiaTopicTag(
UNFOLLOW -> unFollowText() UNFOLLOW -> unFollowText()
BROWSE -> browseText() BROWSE -> browseText()
} }
} },
) )
} }
} }
@ -94,6 +93,7 @@ fun NiaTopicTag(
*/ */
object NiaTagDefaults { object NiaTagDefaults {
const val UnfollowedTopicTagContainerAlpha = 0.5f const val UnfollowedTopicTagContainerAlpha = 0.5f
// TODO: File bug // TODO: File bug
// Button disabled container alpha value not exposed by ButtonDefaults // Button disabled container alpha value not exposed by ButtonDefaults
const val DisabledTopicTagContainerAlpha = 0.12f const val DisabledTopicTagContainerAlpha = 0.12f

@ -46,7 +46,7 @@ fun NiaTopAppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
onNavigationClick: () -> Unit = {}, onNavigationClick: () -> Unit = {},
onActionClick: () -> Unit = {} onActionClick: () -> Unit = {},
) { ) {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { Text(text = stringResource(id = titleRes)) }, title = { Text(text = stringResource(id = titleRes)) },
@ -55,7 +55,7 @@ fun NiaTopAppBar(
Icon( Icon(
imageVector = navigationIcon, imageVector = navigationIcon,
contentDescription = navigationIconContentDescription, contentDescription = navigationIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface,
) )
} }
}, },
@ -64,7 +64,7 @@ fun NiaTopAppBar(
Icon( Icon(
imageVector = actionIcon, imageVector = actionIcon,
contentDescription = actionIconContentDescription, contentDescription = actionIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface,
) )
} }
}, },
@ -84,7 +84,7 @@ fun NiaTopAppBar(
actionIconContentDescription: String?, actionIconContentDescription: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
onActionClick: () -> Unit = {} onActionClick: () -> Unit = {},
) { ) {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { Text(text = stringResource(id = titleRes)) }, title = { Text(text = stringResource(id = titleRes)) },
@ -93,7 +93,7 @@ fun NiaTopAppBar(
Icon( Icon(
imageVector = actionIcon, imageVector = actionIcon,
contentDescription = actionIconContentDescription, contentDescription = actionIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface,
) )
} }
}, },
@ -111,6 +111,6 @@ private fun NiaTopAppBarPreview() {
navigationIcon = NiaIcons.Search, navigationIcon = NiaIcons.Search,
navigationIconContentDescription = "Navigation icon", navigationIconContentDescription = "Navigation icon",
actionIcon = NiaIcons.MoreVert, actionIcon = NiaIcons.MoreVert,
actionIconContentDescription = "Action icon" actionIconContentDescription = "Action icon",
) )
} }

@ -49,25 +49,25 @@ fun NiaViewToggleButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
compactText: @Composable () -> Unit, compactText: @Composable () -> Unit,
expandedText: @Composable () -> Unit expandedText: @Composable () -> Unit,
) { ) {
TextButton( TextButton(
onClick = { onExpandedChange(!expanded) }, onClick = { onExpandedChange(!expanded) },
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground contentColor = MaterialTheme.colorScheme.onBackground,
), ),
contentPadding = NiaViewToggleDefaults.ViewToggleButtonContentPadding contentPadding = NiaViewToggleDefaults.ViewToggleButtonContentPadding,
) { ) {
NiaViewToggleButtonContent( NiaViewToggleButtonContent(
text = if (expanded) expandedText else compactText, text = if (expanded) expandedText else compactText,
trailingIcon = { trailingIcon = {
Icon( Icon(
imageVector = if (expanded) NiaIcons.ViewDay else NiaIcons.ShortText, imageVector = if (expanded) NiaIcons.ViewDay else NiaIcons.ShortText,
contentDescription = null contentDescription = null,
) )
} },
) )
} }
} }
@ -91,8 +91,8 @@ private fun NiaViewToggleButtonContent(
ButtonDefaults.IconSpacing ButtonDefaults.IconSpacing
} else { } else {
0.dp 0.dp
} },
) ),
) { ) {
ProvideTextStyle(value = MaterialTheme.typography.labelSmall) { ProvideTextStyle(value = MaterialTheme.typography.labelSmall) {
text() text()
@ -116,6 +116,6 @@ object NiaViewToggleDefaults {
start = 16.dp, start = 16.dp,
top = 8.dp, top = 8.dp,
end = 12.dp, end = 12.dp,
bottom = 8.dp bottom = 8.dp,
) )
} }

@ -27,7 +27,7 @@ import androidx.compose.ui.unit.Dp
@Immutable @Immutable
data class BackgroundTheme( data class BackgroundTheme(
val color: Color = Color.Unspecified, val color: Color = Color.Unspecified,
val tonalElevation: Dp = Dp.Unspecified val tonalElevation: Dp = Dp.Unspecified,
) )
/** /**

@ -31,7 +31,7 @@ import androidx.compose.ui.graphics.Color
data class GradientColors( data class GradientColors(
val top: Color = Color.Unspecified, val top: Color = Color.Unspecified,
val bottom: Color = Color.Unspecified, val bottom: Color = Color.Unspecified,
val container: Color = Color.Unspecified val container: Color = Color.Unspecified,
) )
/** /**

@ -61,7 +61,7 @@ val LightDefaultColorScheme = lightColorScheme(
onSurfaceVariant = PurpleGray30, onSurfaceVariant = PurpleGray30,
inverseSurface = DarkPurpleGray20, inverseSurface = DarkPurpleGray20,
inverseOnSurface = DarkPurpleGray95, inverseOnSurface = DarkPurpleGray95,
outline = PurpleGray50 outline = PurpleGray50,
) )
/** /**
@ -93,7 +93,7 @@ val DarkDefaultColorScheme = darkColorScheme(
onSurfaceVariant = PurpleGray80, onSurfaceVariant = PurpleGray80,
inverseSurface = DarkPurpleGray90, inverseSurface = DarkPurpleGray90,
inverseOnSurface = DarkPurpleGray10, inverseOnSurface = DarkPurpleGray10,
outline = PurpleGray60 outline = PurpleGray60,
) )
/** /**
@ -125,7 +125,7 @@ val LightAndroidColorScheme = lightColorScheme(
onSurfaceVariant = GreenGray30, onSurfaceVariant = GreenGray30,
inverseSurface = DarkGreenGray20, inverseSurface = DarkGreenGray20,
inverseOnSurface = DarkGreenGray95, inverseOnSurface = DarkGreenGray95,
outline = GreenGray50 outline = GreenGray50,
) )
/** /**
@ -157,7 +157,7 @@ val DarkAndroidColorScheme = darkColorScheme(
onSurfaceVariant = GreenGray80, onSurfaceVariant = GreenGray80,
inverseSurface = DarkGreenGray90, inverseSurface = DarkGreenGray90,
inverseOnSurface = DarkGreenGray10, inverseOnSurface = DarkGreenGray10,
outline = GreenGray60 outline = GreenGray60,
) )
/** /**
@ -194,7 +194,7 @@ fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
androidTheme: Boolean = false, androidTheme: Boolean = false,
disableDynamicTheming: Boolean = true, disableDynamicTheming: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit,
) { ) {
// Color scheme // Color scheme
val colorScheme = when { val colorScheme = when {
@ -211,7 +211,7 @@ fun NiaTheme(
val defaultGradientColors = GradientColors( val defaultGradientColors = GradientColors(
top = colorScheme.inverseOnSurface, top = colorScheme.inverseOnSurface,
bottom = colorScheme.primaryContainer, bottom = colorScheme.primaryContainer,
container = colorScheme.surface container = colorScheme.surface,
) )
val gradientColors = when { val gradientColors = when {
androidTheme -> if (darkTheme) DarkAndroidGradientColors else LightAndroidGradientColors androidTheme -> if (darkTheme) DarkAndroidGradientColors else LightAndroidGradientColors
@ -221,7 +221,7 @@ fun NiaTheme(
// Background theme // Background theme
val defaultBackgroundTheme = BackgroundTheme( val defaultBackgroundTheme = BackgroundTheme(
color = colorScheme.surface, color = colorScheme.surface,
tonalElevation = 2.dp tonalElevation = 2.dp,
) )
val backgroundTheme = when { val backgroundTheme = when {
androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme
@ -236,12 +236,12 @@ fun NiaTheme(
CompositionLocalProvider( CompositionLocalProvider(
LocalGradientColors provides gradientColors, LocalGradientColors provides gradientColors,
LocalBackgroundTheme provides backgroundTheme, LocalBackgroundTheme provides backgroundTheme,
LocalTintTheme provides tintTheme LocalTintTheme provides tintTheme,
) { ) {
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = NiaTypography, typography = NiaTypography,
content = content content = content,
) )
} }
} }

@ -29,90 +29,90 @@ internal val NiaTypography = Typography(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 57.sp, fontSize = 57.sp,
lineHeight = 64.sp, lineHeight = 64.sp,
letterSpacing = (-0.25).sp letterSpacing = (-0.25).sp,
), ),
displayMedium = TextStyle( displayMedium = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 45.sp, fontSize = 45.sp,
lineHeight = 52.sp, lineHeight = 52.sp,
letterSpacing = 0.sp letterSpacing = 0.sp,
), ),
displaySmall = TextStyle( displaySmall = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 36.sp, fontSize = 36.sp,
lineHeight = 44.sp, lineHeight = 44.sp,
letterSpacing = 0.sp letterSpacing = 0.sp,
), ),
headlineLarge = TextStyle( headlineLarge = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 32.sp, fontSize = 32.sp,
lineHeight = 40.sp, lineHeight = 40.sp,
letterSpacing = 0.sp letterSpacing = 0.sp,
), ),
headlineMedium = TextStyle( headlineMedium = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 28.sp, fontSize = 28.sp,
lineHeight = 36.sp, lineHeight = 36.sp,
letterSpacing = 0.sp letterSpacing = 0.sp,
), ),
headlineSmall = TextStyle( headlineSmall = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 24.sp, fontSize = 24.sp,
lineHeight = 32.sp, lineHeight = 32.sp,
letterSpacing = 0.sp letterSpacing = 0.sp,
), ),
titleLarge = TextStyle( titleLarge = TextStyle(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 22.sp, fontSize = 22.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
letterSpacing = 0.sp letterSpacing = 0.sp,
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 18.sp, fontSize = 18.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.1.sp letterSpacing = 0.1.sp,
), ),
titleSmall = TextStyle( titleSmall = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp letterSpacing = 0.1.sp,
), ),
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp,
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.25.sp letterSpacing = 0.25.sp,
), ),
bodySmall = TextStyle( bodySmall = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.4.sp letterSpacing = 0.4.sp,
), ),
labelLarge = TextStyle( labelLarge = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp letterSpacing = 0.1.sp,
), ),
labelMedium = TextStyle( labelMedium = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp,
), ),
labelSmall = TextStyle( labelSmall = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 10.sp, fontSize = 10.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.sp letterSpacing = 0.sp,
) ),
) )

@ -21,16 +21,16 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserDataReposit
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NAME import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NAME
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NONE import com.google.samples.apps.nowinandroid.core.domain.TopicSortField.NONE
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import javax.inject.Inject
/** /**
* A use case which obtains a list of topics with their followed state. * A use case which obtains a list of topics with their followed state.
*/ */
class GetFollowableTopicsUseCase @Inject constructor( class GetFollowableTopicsUseCase @Inject constructor(
private val topicsRepository: TopicsRepository, private val topicsRepository: TopicsRepository,
private val userDataRepository: UserDataRepository private val userDataRepository: UserDataRepository,
) { ) {
/** /**
* Returns a list of topics with their associated followed state. * Returns a list of topics with their associated followed state.
@ -40,13 +40,13 @@ class GetFollowableTopicsUseCase @Inject constructor(
operator fun invoke(sortBy: TopicSortField = NONE): Flow<List<FollowableTopic>> { operator fun invoke(sortBy: TopicSortField = NONE): Flow<List<FollowableTopic>> {
return combine( return combine(
userDataRepository.userData, userDataRepository.userData,
topicsRepository.getTopics() topicsRepository.getTopics(),
) { userData, topics -> ) { userData, topics ->
val followedTopics = topics val followedTopics = topics
.map { topic -> .map { topic ->
FollowableTopic( FollowableTopic(
topic = topic, topic = topic,
isFollowed = topic.id in userData.followedTopics isFollowed = topic.id in userData.followedTopics,
) )
} }
when (sortBy) { when (sortBy) {

@ -22,10 +22,10 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import javax.inject.Inject
/** /**
* A use case responsible for obtaining news resources with their associated bookmarked (also known * A use case responsible for obtaining news resources with their associated bookmarked (also known
@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.filterNot
*/ */
class GetUserNewsResourcesUseCase @Inject constructor( class GetUserNewsResourcesUseCase @Inject constructor(
private val newsRepository: NewsRepository, private val newsRepository: NewsRepository,
private val userDataRepository: UserDataRepository private val userDataRepository: UserDataRepository,
) { ) {
/** /**
* Returns a list of UserNewsResources which match the supplied set of topic ids. * Returns a list of UserNewsResources which match the supplied set of topic ids.
@ -42,7 +42,7 @@ class GetUserNewsResourcesUseCase @Inject constructor(
* this is empty the list of news resources will not be filtered. * this is empty the list of news resources will not be filtered.
*/ */
operator fun invoke( operator fun invoke(
filterTopicIds: Set<String> = emptySet() filterTopicIds: Set<String> = emptySet(),
): Flow<List<UserNewsResource>> = ): Flow<List<UserNewsResource>> =
if (filterTopicIds.isEmpty()) { if (filterTopicIds.isEmpty()) {
newsRepository.getNewsResources() newsRepository.getNewsResources()
@ -52,7 +52,7 @@ class GetUserNewsResourcesUseCase @Inject constructor(
} }
private fun Flow<List<NewsResource>>.mapToUserNewsResources( private fun Flow<List<NewsResource>>.mapToUserNewsResources(
userDataStream: Flow<UserData> userDataStream: Flow<UserData>,
): Flow<List<UserNewsResource>> = ): Flow<List<UserNewsResource>> =
filterNot { it.isEmpty() } filterNot { it.isEmpty() }
.combine(userDataStream) { newsResources, userData -> .combine(userDataStream) { newsResources, userData ->

@ -24,20 +24,20 @@ import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
*/ */
data class FollowableTopic( // TODO consider changing to UserTopic and flattening data class FollowableTopic( // TODO consider changing to UserTopic and flattening
val topic: Topic, val topic: Topic,
val isFollowed: Boolean val isFollowed: Boolean,
) )
val previewFollowableTopics = listOf( val previewFollowableTopics = listOf(
FollowableTopic( FollowableTopic(
previewTopics[0], previewTopics[0],
isFollowed = false isFollowed = false,
), ),
FollowableTopic( FollowableTopic(
previewTopics[1], previewTopics[1],
isFollowed = true isFollowed = true,
), ),
FollowableTopic( FollowableTopic(
previewTopics[2], previewTopics[2],
isFollowed = false isFollowed = false,
) ),
) )

@ -42,7 +42,7 @@ data class UserNewsResource internal constructor(
val publishDate: Instant, val publishDate: Instant,
val type: NewsResourceType, val type: NewsResourceType,
val followableTopics: List<FollowableTopic>, val followableTopics: List<FollowableTopic>,
val isSaved: Boolean val isSaved: Boolean,
) { ) {
constructor(newsResource: NewsResource, userData: UserData) : this( constructor(newsResource: NewsResource, userData: UserData) : this(
id = newsResource.id, id = newsResource.id,
@ -55,10 +55,10 @@ data class UserNewsResource internal constructor(
followableTopics = newsResource.topics.map { topic -> followableTopics = newsResource.topics.map { topic ->
FollowableTopic( FollowableTopic(
topic = topic, topic = topic,
isFollowed = userData.followedTopics.contains(topic.id) isFollowed = userData.followedTopics.contains(topic.id),
) )
}, },
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id) isSaved = userData.bookmarkedNewsResources.contains(newsResource.id),
) )
} }
@ -80,11 +80,11 @@ val previewUserNewsResources = listOf(
hour = 23, hour = 23,
minute = 0, minute = 0,
second = 0, second = 0,
nanosecond = 0 nanosecond = 0,
).toInstant(TimeZone.UTC), ).toInstant(TimeZone.UTC),
type = Codelab, type = Codelab,
followableTopics = listOf(previewFollowableTopics[1]), followableTopics = listOf(previewFollowableTopics[1]),
isSaved = true isSaved = true,
), ),
UserNewsResource( UserNewsResource(
id = "2", id = "2",
@ -98,7 +98,7 @@ val previewUserNewsResources = listOf(
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video, type = Video,
followableTopics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]), followableTopics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]),
isSaved = false isSaved = false,
), ),
UserNewsResource( UserNewsResource(
id = "3", id = "3",
@ -112,7 +112,7 @@ val previewUserNewsResources = listOf(
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"), publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video, type = Video,
followableTopics = listOf(previewFollowableTopics[2]), followableTopics = listOf(previewFollowableTopics[2]),
isSaved = false isSaved = false,
), ),
UserNewsResource( UserNewsResource(
id = "4", id = "4",
@ -124,6 +124,6 @@ val previewUserNewsResources = listOf(
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"), publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = Unknown, type = Unknown,
followableTopics = listOf(previewFollowableTopics[2]), followableTopics = listOf(previewFollowableTopics[2]),
isSaved = true isSaved = true,
) ),
) )

@ -22,11 +22,11 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class GetFollowableTopicsUseCaseTest { class GetFollowableTopicsUseCaseTest {
@ -38,12 +38,11 @@ class GetFollowableTopicsUseCaseTest {
val useCase = GetFollowableTopicsUseCase( val useCase = GetFollowableTopicsUseCase(
topicsRepository, topicsRepository,
userDataRepository userDataRepository,
) )
@Test @Test
fun whenNoParams_followableTopicsAreReturnedWithNoSorting() = runTest { fun whenNoParams_followableTopicsAreReturnedWithNoSorting() = runTest {
// Obtain a stream of followable topics. // Obtain a stream of followable topics.
val followableTopics = useCase() val followableTopics = useCase()
@ -58,16 +57,15 @@ class GetFollowableTopicsUseCaseTest {
FollowableTopic(testTopics[1], false), FollowableTopic(testTopics[1], false),
FollowableTopic(testTopics[2], true), FollowableTopic(testTopics[2], true),
), ),
followableTopics.first() followableTopics.first(),
) )
} }
@Test @Test
fun whenSortOrderIsByName_topicsSortedByNameAreReturned() = runTest { fun whenSortOrderIsByName_topicsSortedByNameAreReturned() = runTest {
// Obtain a stream of followable topics, sorted by name. // Obtain a stream of followable topics, sorted by name.
val followableTopics = useCase( val followableTopics = useCase(
sortBy = NAME sortBy = NAME,
) )
// Send some test topics and their followed state. // Send some test topics and their followed state.
@ -81,7 +79,7 @@ class GetFollowableTopicsUseCaseTest {
.sortedBy { it.name } .sortedBy { it.name }
.map { .map {
FollowableTopic(it, false) FollowableTopic(it, false)
} },
) )
} }
} }

@ -24,12 +24,12 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepo
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class GetUserNewsResourcesUseCaseTest { class GetUserNewsResourcesUseCaseTest {
@ -43,7 +43,6 @@ class GetUserNewsResourcesUseCaseTest {
@Test @Test
fun whenNoFilters_allNewsResourcesAreReturned() = runTest { fun whenNoFilters_allNewsResourcesAreReturned() = runTest {
// Obtain the user news resources stream. // Obtain the user news resources stream.
val userNewsResources = useCase() val userNewsResources = useCase()
@ -53,7 +52,7 @@ class GetUserNewsResourcesUseCaseTest {
// Construct the test user data with bookmarks and followed topics. // Construct the test user data with bookmarks and followed topics.
val userData = emptyUserData.copy( val userData = emptyUserData.copy(
bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id), bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id),
followedTopics = setOf(sampleTopic1.id) followedTopics = setOf(sampleTopic1.id),
) )
userDataRepository.setUserData(userData) userDataRepository.setUserData(userData)
@ -61,13 +60,12 @@ class GetUserNewsResourcesUseCaseTest {
// Check that the correct news resources are returned with their bookmarked state. // Check that the correct news resources are returned with their bookmarked state.
assertEquals( assertEquals(
sampleNewsResources.mapToUserNewsResources(userData), sampleNewsResources.mapToUserNewsResources(userData),
userNewsResources.first() userNewsResources.first(),
) )
} }
@Test @Test
fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest { fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest {
// Obtain a stream of user news resources for the given topic id. // Obtain a stream of user news resources for the given topic id.
val userNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id)) val userNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id))
@ -80,7 +78,7 @@ class GetUserNewsResourcesUseCaseTest {
sampleNewsResources sampleNewsResources
.filter { it.topics.contains(sampleTopic1) } .filter { it.topics.contains(sampleTopic1) }
.mapToUserNewsResources(emptyUserData), .mapToUserNewsResources(emptyUserData),
userNewsResources.first() userNewsResources.first(),
) )
} }
} }
@ -115,7 +113,7 @@ private val sampleNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video, type = Video,
topics = listOf(sampleTopic1) topics = listOf(sampleTopic1),
), ),
NewsResource( NewsResource(
id = "2", id = "2",
@ -127,7 +125,7 @@ private val sampleNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg", headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"), publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video, type = Video,
topics = listOf(sampleTopic1, sampleTopic2) topics = listOf(sampleTopic1, sampleTopic2),
), ),
NewsResource( NewsResource(
id = "3", id = "3",
@ -137,6 +135,6 @@ private val sampleNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/r5JgIyS3t3s/maxresdefault.jpg", headerImageUrl = "https://i.ytimg.com/vi/r5JgIyS3t3s/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-08T00:00:00.000Z"), publishDate = Instant.parse("2021-11-08T00:00:00.000Z"),
type = Video, type = Video,
topics = listOf(sampleTopic2) topics = listOf(sampleTopic2),
), ),
) )

@ -38,7 +38,6 @@ class UserNewsResourceTest {
*/ */
@Test @Test
fun userNewsResourcesAreConstructedFromNewsResourcesAndUserData() { fun userNewsResourcesAreConstructedFromNewsResourcesAndUserData() {
val newsResource1 = NewsResource( val newsResource1 = NewsResource(
id = "N1", id = "N1",
title = "Test news title", title = "Test news title",
@ -54,7 +53,7 @@ class UserNewsResourceTest {
shortDescription = "Topic 1 short description", shortDescription = "Topic 1 short description",
longDescription = "Topic 1 long description", longDescription = "Topic 1 long description",
url = "Topic 1 URL", url = "Topic 1 URL",
imageUrl = "Topic 1 image URL" imageUrl = "Topic 1 image URL",
), ),
Topic( Topic(
id = "T2", id = "T2",
@ -62,9 +61,9 @@ class UserNewsResourceTest {
shortDescription = "Topic 2 short description", shortDescription = "Topic 2 short description",
longDescription = "Topic 2 long description", longDescription = "Topic 2 long description",
url = "Topic 2 URL", url = "Topic 2 URL",
imageUrl = "Topic 2 image URL" imageUrl = "Topic 2 image URL",
),
), ),
)
) )
val userData = UserData( val userData = UserData(
@ -73,7 +72,7 @@ class UserNewsResourceTest {
themeBrand = DEFAULT, themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM, darkThemeConfig = FOLLOW_SYSTEM,
useDynamicColor = false, useDynamicColor = false,
shouldHideOnboarding = true shouldHideOnboarding = true,
) )
val userNewsResource = UserNewsResource(newsResource1, userData) val userNewsResource = UserNewsResource(newsResource1, userData)
@ -89,11 +88,10 @@ class UserNewsResourceTest {
// Check that each Topic has been converted to a FollowedTopic correctly. // Check that each Topic has been converted to a FollowedTopic correctly.
assertEquals(newsResource1.topics.size, userNewsResource.followableTopics.size) assertEquals(newsResource1.topics.size, userNewsResource.followableTopics.size)
for (topic in newsResource1.topics) { for (topic in newsResource1.topics) {
// Construct the expected FollowableTopic. // Construct the expected FollowableTopic.
val followableTopic = FollowableTopic( val followableTopic = FollowableTopic(
topic = topic, topic = topic,
isFollowed = userData.followedTopics.contains(topic.id) isFollowed = userData.followedTopics.contains(topic.id),
) )
assertTrue(userNewsResource.followableTopics.contains(followableTopic)) assertTrue(userNewsResource.followableTopics.contains(followableTopic))
} }
@ -101,7 +99,7 @@ class UserNewsResourceTest {
// Check that the saved flag is set correctly. // Check that the saved flag is set correctly.
assertEquals( assertEquals(
userData.bookmarkedNewsResources.contains(newsResource1.id), userData.bookmarkedNewsResources.contains(newsResource1.id),
userNewsResource.isSaved userNewsResource.isSaved,
) )
} }
} }

@ -37,7 +37,7 @@ data class NewsResource(
val headerImageUrl: String?, val headerImageUrl: String?,
val publishDate: Instant, val publishDate: Instant,
val type: NewsResourceType, val type: NewsResourceType,
val topics: List<Topic> val topics: List<Topic>,
) )
val previewNewsResources = listOf( val previewNewsResources = listOf(
@ -54,10 +54,10 @@ val previewNewsResources = listOf(
hour = 23, hour = 23,
minute = 0, minute = 0,
second = 0, second = 0,
nanosecond = 0 nanosecond = 0,
).toInstant(TimeZone.UTC), ).toInstant(TimeZone.UTC),
type = Codelab, type = Codelab,
topics = listOf(previewTopics[1]) topics = listOf(previewTopics[1]),
), ),
NewsResource( NewsResource(
id = "2", id = "2",
@ -70,7 +70,7 @@ val previewNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video, type = Video,
topics = listOf(previewTopics[0], previewTopics[1]) topics = listOf(previewTopics[0], previewTopics[1]),
), ),
NewsResource( NewsResource(
id = "3", id = "3",
@ -83,7 +83,7 @@ val previewNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg", headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"), publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video, type = Video,
topics = listOf(previewTopics[2]) topics = listOf(previewTopics[2]),
), ),
NewsResource( NewsResource(
id = "4", id = "4",
@ -94,6 +94,6 @@ val previewNewsResources = listOf(
headerImageUrl = "", headerImageUrl = "",
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"), publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = Unknown, type = Unknown,
topics = listOf(previewTopics[2]) topics = listOf(previewTopics[2]),
) ),
) )

@ -23,53 +23,53 @@ enum class NewsResourceType(
val serializedName: String, val serializedName: String,
val displayText: String, val displayText: String,
// TODO: descriptions should probably be string resources // TODO: descriptions should probably be string resources
val description: String val description: String,
) { ) {
Video( Video(
serializedName = "Video 📺", serializedName = "Video 📺",
displayText = "Video 📺", displayText = "Video 📺",
description = "A video published on YouTube" description = "A video published on YouTube",
), ),
APIChange( APIChange(
serializedName = "API change", serializedName = "API change",
displayText = "API change", displayText = "API change",
description = "An addition, deprecation or change to the Android platform APIs." description = "An addition, deprecation or change to the Android platform APIs.",
), ),
Article( Article(
serializedName = "Article 📚", serializedName = "Article 📚",
displayText = "Article 📚", displayText = "Article 📚",
description = "An article, typically on Medium or the official Android blog" description = "An article, typically on Medium or the official Android blog",
), ),
Codelab( Codelab(
serializedName = "Codelab", serializedName = "Codelab",
displayText = "Codelab", displayText = "Codelab",
description = "A new or updated codelab" description = "A new or updated codelab",
), ),
Podcast( Podcast(
serializedName = "Podcast 🎙", serializedName = "Podcast 🎙",
displayText = "Podcast 🎙", displayText = "Podcast 🎙",
description = "A podcast" description = "A podcast",
), ),
Docs( Docs(
serializedName = "Docs 📑", serializedName = "Docs 📑",
displayText = "Docs 📑", displayText = "Docs 📑",
description = "A new or updated piece of documentation" description = "A new or updated piece of documentation",
), ),
Event( Event(
serializedName = "Event 📆", serializedName = "Event 📆",
displayText = "Event 📆", displayText = "Event 📆",
description = "Information about a developer event e.g. Android Developer Summit" description = "Information about a developer event e.g. Android Developer Summit",
), ),
DAC( DAC(
serializedName = "DAC", serializedName = "DAC",
displayText = "DAC", displayText = "DAC",
description = "Android version features - Information about features in an Android" description = "Android version features - Information about features in an Android",
), ),
Unknown( Unknown(
serializedName = "Unknown", serializedName = "Unknown",
displayText = "Unknown", displayText = "Unknown",
description = "Unknown" description = "Unknown",
) ),
} }
fun String?.asNewsResourceType() = when (this) { fun String?.asNewsResourceType() = when (this) {

@ -37,7 +37,7 @@ val previewTopics = listOf(
shortDescription = "News we want everyone to see", shortDescription = "News we want everyone to see",
longDescription = "Stay up to date with the latest events and announcements from Android!", longDescription = "Stay up to date with the latest events and announcements from Android!",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Headlines.svg?alt=media&token=506faab0-617a-4668-9e63-4a2fb996603f", imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Headlines.svg?alt=media&token=506faab0-617a-4668-9e63-4a2fb996603f",
url = "" url = "",
), ),
Topic( Topic(
id = "3", id = "3",
@ -45,7 +45,7 @@ val previewTopics = listOf(
shortDescription = "Material Design, Navigation, Text, Paging, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets", shortDescription = "Material Design, Navigation, Text, Paging, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets",
longDescription = "Learn how to optimize your app's user interface - everything that users can see and interact with. Stay up to date on tocpis such as Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets, and many more!", longDescription = "Learn how to optimize your app's user interface - everything that users can see and interact with. Stay up to date on tocpis such as Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets, and many more!",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_UI.svg?alt=media&token=0ee1842b-12e8-435f-87ba-a5bb02c47594", imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_UI.svg?alt=media&token=0ee1842b-12e8-435f-87ba-a5bb02c47594",
url = "" url = "",
), ),
Topic( Topic(
id = "4", id = "4",
@ -53,6 +53,6 @@ val previewTopics = listOf(
shortDescription = "CI, Espresso, TestLab, etc", shortDescription = "CI, Espresso, TestLab, etc",
longDescription = "Testing is an integral part of the app development process. By running tests against your app consistently, you can verify your app's correctness, functional behavior, and usability before you release it publicly. Stay up to date on the latest tricks in CI, Espresso, and Firebase TestLab.", longDescription = "Testing is an integral part of the app development process. By running tests against your app consistently, you can verify your app's correctness, functional behavior, and usability before you release it publicly. Stay up to date on the latest tricks in CI, Espresso, and Firebase TestLab.",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Testing.svg?alt=media&token=a11533c4-7cc8-4b11-91a3-806158ebf428", imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Testing.svg?alt=media&token=a11533c4-7cc8-4b11-91a3-806158ebf428",
url = "" url = "",
), ),
) )

@ -25,5 +25,5 @@ data class UserData(
val themeBrand: ThemeBrand, val themeBrand: ThemeBrand,
val darkThemeConfig: DarkThemeConfig, val darkThemeConfig: DarkThemeConfig,
val useDynamicColor: Boolean, val useDynamicColor: Boolean,
val shouldHideOnboarding: Boolean val shouldHideOnboarding: Boolean,
) )

@ -23,8 +23,8 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

@ -23,12 +23,12 @@ import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import javax.inject.Inject
/** /**
* [NiaNetworkDataSource] implementation that provides static news resources to aid development * [NiaNetworkDataSource] implementation that provides static news resources to aid development
@ -69,7 +69,7 @@ class FakeNiaNetworkDataSource @Inject constructor(
* [NetworkChangeList.id] * [NetworkChangeList.id]
*/ */
private fun <T> List<T>.mapToChangeList( private fun <T> List<T>.mapToChangeList(
idGetter: (T) -> String idGetter: (T) -> String,
) = mapIndexed { index, item -> ) = mapIndexed { index, item ->
NetworkChangeList( NetworkChangeList(
id = idGetter(item), id = idGetter(item),

@ -31,7 +31,7 @@ object InstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
serialName = "Instant", serialName = "Instant",
kind = STRING kind = STRING,
) )
override fun serialize(encoder: Encoder, value: Instant) = override fun serialize(encoder: Encoder, value: Instant) =

@ -31,7 +31,7 @@ object NewsResourceTypeSerializer : KSerializer<NewsResourceType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
serialName = "type", serialName = "type",
kind = STRING kind = STRING,
) )
override fun serialize(encoder: Encoder, value: NewsResourceType) = override fun serialize(encoder: Encoder, value: NewsResourceType) =

@ -22,8 +22,6 @@ import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -33,6 +31,8 @@ import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
import javax.inject.Inject
import javax.inject.Singleton
/** /**
* Retrofit API declaration for NIA Network API * Retrofit API declaration for NIA Network API
@ -66,7 +66,7 @@ private const val NiaBaseUrl = BuildConfig.BACKEND_URL
*/ */
@Serializable @Serializable
private data class NetworkResponse<T>( private data class NetworkResponse<T>(
val data: T val data: T,
) )
/** /**
@ -74,7 +74,7 @@ private data class NetworkResponse<T>(
*/ */
@Singleton @Singleton
class RetrofitNiaNetwork @Inject constructor( class RetrofitNiaNetwork @Inject constructor(
networkJson: Json networkJson: Json,
) : NiaNetworkDataSource { ) : NiaNetworkDataSource {
private val networkApi = Retrofit.Builder() private val networkApi = Retrofit.Builder()
@ -85,13 +85,13 @@ class RetrofitNiaNetwork @Inject constructor(
// TODO: Decide logging logic // TODO: Decide logging logic
HttpLoggingInterceptor().apply { HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY) setLevel(HttpLoggingInterceptor.Level.BODY)
} },
) )
.build() .build(),
) )
.addConverterFactory( .addConverterFactory(
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
networkJson.asConverterFactory("application/json".toMediaType()) networkJson.asConverterFactory("application/json".toMediaType()),
) )
.build() .build()
.create(RetrofitNiaNetworkApi::class.java) .create(RetrofitNiaNetworkApi::class.java)

@ -20,7 +20,6 @@ import JvmUnitTestFakeAssetManager
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Codelab import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Codelab
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlin.test.assertEquals
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
@ -29,6 +28,7 @@ import kotlinx.datetime.toInstant
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class FakeNiaNetworkDataSourceTest { class FakeNiaNetworkDataSourceTest {
@ -41,7 +41,7 @@ class FakeNiaNetworkDataSourceTest {
subject = FakeNiaNetworkDataSource( subject = FakeNiaNetworkDataSource(
ioDispatcher = testDispatcher, ioDispatcher = testDispatcher,
networkJson = Json { ignoreUnknownKeys = true }, networkJson = Json { ignoreUnknownKeys = true },
assets = JvmUnitTestFakeAssetManager assets = JvmUnitTestFakeAssetManager,
) )
} }
@ -55,10 +55,10 @@ class FakeNiaNetworkDataSourceTest {
shortDescription = "News you'll definitely be interested in", shortDescription = "News you'll definitely be interested in",
longDescription = "The latest events and announcements from the world of Android development.", longDescription = "The latest events and announcements from the world of Android development.",
url = "", url = "",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Headlines.svg?alt=media&token=506faab0-617a-4668-9e63-4a2fb996603f" imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Headlines.svg?alt=media&token=506faab0-617a-4668-9e63-4a2fb996603f",
), ),
/* ktlint-enable max-line-length */ /* ktlint-enable max-line-length */
subject.getTopics().first() subject.getTopics().first(),
) )
} }
@ -79,13 +79,13 @@ class FakeNiaNetworkDataSourceTest {
hour = 23, hour = 23,
minute = 0, minute = 0,
second = 0, second = 0,
nanosecond = 0 nanosecond = 0,
).toInstant(TimeZone.UTC), ).toInstant(TimeZone.UTC),
type = Codelab, type = Codelab,
topics = listOf("2", "3", "10"), topics = listOf("2", "3", "10"),
), ),
/* ktlint-enable max-line-length */ /* ktlint-enable max-line-length */
subject.getNewsResources().find { it.id == "125" } subject.getNewsResources().find { it.id == "125" },
) )
} }
} }

@ -17,9 +17,9 @@
package com.google.samples.apps.nowinandroid.core.network.model.util package com.google.samples.apps.nowinandroid.core.network.model.util
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlin.test.assertEquals
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
class NewsResourceTypeSerializerTest { class NewsResourceTypeSerializerTest {
@ -27,7 +27,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_video() { fun test_news_resource_serializer_video() {
assertEquals( assertEquals(
NewsResourceType.Video, NewsResourceType.Video,
Json.decodeFromString(NewsResourceTypeSerializer, """"Video 📺"""") Json.decodeFromString(NewsResourceTypeSerializer, """"Video 📺""""),
) )
} }
@ -35,7 +35,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_article() { fun test_news_resource_serializer_article() {
assertEquals( assertEquals(
NewsResourceType.Article, NewsResourceType.Article,
Json.decodeFromString(NewsResourceTypeSerializer, """"Article 📚"""") Json.decodeFromString(NewsResourceTypeSerializer, """"Article 📚""""),
) )
} }
@ -43,7 +43,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_api_change() { fun test_news_resource_serializer_api_change() {
assertEquals( assertEquals(
NewsResourceType.APIChange, NewsResourceType.APIChange,
Json.decodeFromString(NewsResourceTypeSerializer, """"API change"""") Json.decodeFromString(NewsResourceTypeSerializer, """"API change""""),
) )
} }
@ -51,7 +51,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_codelab() { fun test_news_resource_serializer_codelab() {
assertEquals( assertEquals(
NewsResourceType.Codelab, NewsResourceType.Codelab,
Json.decodeFromString(NewsResourceTypeSerializer, """"Codelab"""") Json.decodeFromString(NewsResourceTypeSerializer, """"Codelab""""),
) )
} }
@ -59,7 +59,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_podcast() { fun test_news_resource_serializer_podcast() {
assertEquals( assertEquals(
NewsResourceType.Podcast, NewsResourceType.Podcast,
Json.decodeFromString(NewsResourceTypeSerializer, """"Podcast 🎙"""") Json.decodeFromString(NewsResourceTypeSerializer, """"Podcast 🎙""""),
) )
} }
@ -67,7 +67,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_docs() { fun test_news_resource_serializer_docs() {
assertEquals( assertEquals(
NewsResourceType.Docs, NewsResourceType.Docs,
Json.decodeFromString(NewsResourceTypeSerializer, """"Docs 📑"""") Json.decodeFromString(NewsResourceTypeSerializer, """"Docs 📑""""),
) )
} }
@ -75,7 +75,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_event() { fun test_news_resource_serializer_event() {
assertEquals( assertEquals(
NewsResourceType.Event, NewsResourceType.Event,
Json.decodeFromString(NewsResourceTypeSerializer, """"Event 📆"""") Json.decodeFromString(NewsResourceTypeSerializer, """"Event 📆""""),
) )
} }
@ -83,7 +83,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_dac() { fun test_news_resource_serializer_dac() {
assertEquals( assertEquals(
NewsResourceType.DAC, NewsResourceType.DAC,
Json.decodeFromString(NewsResourceTypeSerializer, """"DAC"""") Json.decodeFromString(NewsResourceTypeSerializer, """"DAC""""),
) )
} }
@ -91,7 +91,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_unknown() { fun test_news_resource_serializer_unknown() {
assertEquals( assertEquals(
NewsResourceType.Unknown, NewsResourceType.Unknown,
Json.decodeFromString(NewsResourceTypeSerializer, """"umm"""") Json.decodeFromString(NewsResourceTypeSerializer, """"umm""""),
) )
} }
@ -100,7 +100,7 @@ class NewsResourceTypeSerializerTest {
val json = Json.encodeToString(NewsResourceTypeSerializer, NewsResourceType.Video) val json = Json.encodeToString(NewsResourceTypeSerializer, NewsResourceType.Video)
assertEquals( assertEquals(
NewsResourceType.Video, NewsResourceType.Video,
Json.decodeFromString(NewsResourceTypeSerializer, json) Json.decodeFromString(NewsResourceTypeSerializer, json),
) )
} }
} }

@ -20,9 +20,9 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

@ -31,7 +31,7 @@ val emptyUserData = UserData(
themeBrand = ThemeBrand.DEFAULT, themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
useDynamicColor = false, useDynamicColor = false,
shouldHideOnboarding = false shouldHideOnboarding = false,
) )
class TestUserDataRepository : UserDataRepository { class TestUserDataRepository : UserDataRepository {
@ -50,8 +50,11 @@ class TestUserDataRepository : UserDataRepository {
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) { override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) {
currentUserData.let { current -> currentUserData.let { current ->
val followedTopics = if (followed) current.followedTopics + followedTopicId val followedTopics = if (followed) {
else current.followedTopics - followedTopicId current.followedTopics + followedTopicId
} else {
current.followedTopics - followedTopicId
}
_userData.tryEmit(current.copy(followedTopics = followedTopics)) _userData.tryEmit(current.copy(followedTopics = followedTopics))
} }
@ -59,8 +62,11 @@ class TestUserDataRepository : UserDataRepository {
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) { override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
currentUserData.let { current -> currentUserData.let { current ->
val bookmarkedNews = if (bookmarked) current.bookmarkedNewsResources + newsResourceId val bookmarkedNews = if (bookmarked) {
else current.bookmarkedNewsResources - newsResourceId current.bookmarkedNewsResources + newsResourceId
} else {
current.bookmarkedNewsResources - newsResourceId
}
_userData.tryEmit(current.copy(bookmarkedNewsResources = bookmarkedNews)) _userData.tryEmit(current.copy(bookmarkedNewsResources = bookmarkedNews))
} }

@ -40,7 +40,7 @@ class NewsResourceCardTest {
userNewsResource = newsWithKnownResourceType, userNewsResource = newsWithKnownResourceType,
isBookmarked = false, isBookmarked = false,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {} onClick = {},
) )
dateFormatted = dateFormatted(publishDate = newsWithKnownResourceType.publishDate) dateFormatted = dateFormatted(publishDate = newsWithKnownResourceType.publishDate)
@ -51,8 +51,8 @@ class NewsResourceCardTest {
composeTestRule.activity.getString( composeTestRule.activity.getString(
R.string.card_meta_data_text, R.string.card_meta_data_text,
dateFormatted, dateFormatted,
newsWithKnownResourceType.type.displayText newsWithKnownResourceType.type.displayText,
) ),
) )
.assertExists() .assertExists()
} }
@ -67,7 +67,7 @@ class NewsResourceCardTest {
userNewsResource = newsWithUnknownResourceType, userNewsResource = newsWithUnknownResourceType,
isBookmarked = false, isBookmarked = false,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {} onClick = {},
) )
dateFormatted = dateFormatted(publishDate = newsWithUnknownResourceType.publishDate) dateFormatted = dateFormatted(publishDate = newsWithUnknownResourceType.publishDate)

@ -51,7 +51,7 @@ fun rememberMetricsStateHolder(): Holder {
@Composable @Composable
fun TrackJank( fun TrackJank(
vararg keys: Any?, vararg keys: Any?,
reportMetric: suspend CoroutineScope.(state: Holder) -> Unit reportMetric: suspend CoroutineScope.(state: Holder) -> Unit,
) { ) {
val metrics = rememberMetricsStateHolder() val metrics = rememberMetricsStateHolder()
LaunchedEffect(metrics, *keys) { LaunchedEffect(metrics, *keys) {
@ -66,7 +66,7 @@ fun TrackJank(
@Composable @Composable
fun TrackDisposableJank( fun TrackDisposableJank(
vararg keys: Any?, vararg keys: Any?,
reportMetric: DisposableEffectScope.(state: Holder) -> DisposableEffectResult reportMetric: DisposableEffectScope.(state: Holder) -> DisposableEffectResult,
) { ) {
val metrics = rememberMetricsStateHolder() val metrics = rememberMetricsStateHolder()
DisposableEffect(metrics, *keys) { DisposableEffect(metrics, *keys) {

@ -46,7 +46,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsRes
*/ */
fun LazyGridScope.newsFeed( fun LazyGridScope.newsFeed(
feedState: NewsFeedUiState, feedState: NewsFeedUiState,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
) { ) {
when (feedState) { when (feedState) {
NewsFeedUiState.Loading -> Unit NewsFeedUiState.Loading -> Unit
@ -65,9 +65,9 @@ fun LazyGridScope.newsFeed(
onToggleBookmark = { onToggleBookmark = {
onNewsResourcesCheckedChanged( onNewsResourcesCheckedChanged(
userNewsResource.id, userNewsResource.id,
!userNewsResource.isSaved !userNewsResource.isSaved,
) )
} },
) )
} }
} }
@ -100,7 +100,7 @@ sealed interface NewsFeedUiState {
/** /**
* The list of news resources contained in this feed. * The list of news resources contained in this feed.
*/ */
val feed: List<UserNewsResource> val feed: List<UserNewsResource>,
) : NewsFeedUiState ) : NewsFeedUiState
} }
@ -111,7 +111,7 @@ private fun NewsFeedLoadingPreview() {
LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) { LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) {
newsFeed( newsFeed(
feedState = NewsFeedUiState.Loading, feedState = NewsFeedUiState.Loading,
onNewsResourcesCheckedChanged = { _, _ -> } onNewsResourcesCheckedChanged = { _, _ -> },
) )
} }
} }
@ -125,9 +125,9 @@ private fun NewsFeedContentPreview() {
LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) { LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) {
newsFeed( newsFeed(
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
previewUserNewsResources previewUserNewsResources,
), ),
onNewsResourcesCheckedChanged = { _, _ -> } onNewsResourcesCheckedChanged = { _, _ -> },
) )
} }
} }

@ -52,7 +52,6 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
@ -62,11 +61,12 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
import kotlinx.datetime.Instant import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR
import kotlinx.datetime.toJavaInstant
/** /**
* [NewsResource] card used on the following screens: For You, Saved * [NewsResource] card used on the following screens: For You, Saved
@ -79,7 +79,7 @@ fun NewsResourceCardExpanded(
isBookmarked: Boolean, isBookmarked: Boolean,
onToggleBookmark: () -> Unit, onToggleBookmark: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
val clickActionLabel = stringResource(R.string.card_tap_action) val clickActionLabel = stringResource(R.string.card_tap_action)
Card( Card(
@ -90,7 +90,7 @@ fun NewsResourceCardExpanded(
// Pass null for action to only override the label and not the actual action. // Pass null for action to only override the label and not the actual action.
modifier = modifier.semantics { modifier = modifier.semantics {
onClick(label = clickActionLabel, action = null) onClick(label = clickActionLabel, action = null)
} },
) { ) {
Column { Column {
if (!userNewsResource.headerImageUrl.isNullOrEmpty()) { if (!userNewsResource.headerImageUrl.isNullOrEmpty()) {
@ -99,14 +99,14 @@ fun NewsResourceCardExpanded(
} }
} }
Box( Box(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp),
) { ) {
Column { Column {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Row { Row {
NewsResourceTitle( NewsResourceTitle(
userNewsResource.title, userNewsResource.title,
modifier = Modifier.fillMaxWidth((.8f)) modifier = Modifier.fillMaxWidth((.8f)),
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
BookmarkButton(isBookmarked, onToggleBookmark) BookmarkButton(isBookmarked, onToggleBookmark)
@ -125,7 +125,7 @@ fun NewsResourceCardExpanded(
@Composable @Composable
fun NewsResourceHeaderImage( fun NewsResourceHeaderImage(
headerImageUrl: String? headerImageUrl: String?,
) { ) {
AsyncImage( AsyncImage(
placeholder = if (LocalInspectionMode.current) { placeholder = if (LocalInspectionMode.current) {
@ -140,14 +140,14 @@ fun NewsResourceHeaderImage(
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
model = headerImageUrl, model = headerImageUrl,
// TODO b/226661685: Investigate using alt text of image to populate content description // TODO b/226661685: Investigate using alt text of image to populate content description
contentDescription = null // decorative image contentDescription = null, // decorative image
) )
} }
@Composable @Composable
fun NewsResourceTitle( fun NewsResourceTitle(
newsResourceTitle: String, newsResourceTitle: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
Text(newsResourceTitle, style = MaterialTheme.typography.headlineSmall, modifier = modifier) Text(newsResourceTitle, style = MaterialTheme.typography.headlineSmall, modifier = modifier)
} }
@ -156,7 +156,7 @@ fun NewsResourceTitle(
fun BookmarkButton( fun BookmarkButton(
isBookmarked: Boolean, isBookmarked: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
NiaIconToggleButton( NiaIconToggleButton(
checked = isBookmarked, checked = isBookmarked,
@ -165,15 +165,15 @@ fun BookmarkButton(
icon = { icon = {
Icon( Icon(
painter = painterResource(NiaIcons.BookmarkBorder), painter = painterResource(NiaIcons.BookmarkBorder),
contentDescription = stringResource(R.string.bookmark) contentDescription = stringResource(R.string.bookmark),
) )
}, },
checkedIcon = { checkedIcon = {
Icon( Icon(
painter = painterResource(NiaIcons.Bookmark), painter = painterResource(NiaIcons.Bookmark),
contentDescription = stringResource(R.string.unbookmark) contentDescription = stringResource(R.string.unbookmark),
) )
} },
) )
} }
@ -185,7 +185,7 @@ fun dateFormatted(publishDate: Instant): String {
DisposableEffect(context) { DisposableEffect(context) {
val receiver = TimeZoneBroadcastReceiver( val receiver = TimeZoneBroadcastReceiver(
onTimeZoneChanged = { zoneId = ZoneId.systemDefault() } onTimeZoneChanged = { zoneId = ZoneId.systemDefault() },
) )
receiver.register(context) receiver.register(context)
onDispose { onDispose {
@ -200,7 +200,7 @@ fun dateFormatted(publishDate: Instant): String {
@Composable @Composable
fun NewsResourceMetaData( fun NewsResourceMetaData(
publishDate: Instant, publishDate: Instant,
resourceType: NewsResourceType resourceType: NewsResourceType,
) { ) {
val formattedDate = dateFormatted(publishDate) val formattedDate = dateFormatted(publishDate)
Text( Text(
@ -209,21 +209,21 @@ fun NewsResourceMetaData(
} else { } else {
formattedDate formattedDate
}, },
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall,
) )
} }
@Composable @Composable
fun NewsResourceLink( fun NewsResourceLink(
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
newsResource: NewsResource newsResource: NewsResource,
) { ) {
TODO() TODO()
} }
@Composable @Composable
fun NewsResourceShortDescription( fun NewsResourceShortDescription(
newsResourceShortDescription: String newsResourceShortDescription: String,
) { ) {
Text(newsResourceShortDescription, style = MaterialTheme.typography.bodyLarge) Text(newsResourceShortDescription, style = MaterialTheme.typography.bodyLarge)
} }
@ -231,7 +231,7 @@ fun NewsResourceShortDescription(
@Composable @Composable
fun NewsResourceTopics( fun NewsResourceTopics(
topics: List<FollowableTopic>, topics: List<FollowableTopic>,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
// Store the ID of the Topic which has its "following" menu expanded, if any. // Store the ID of the Topic which has its "following" menu expanded, if any.
// To avoid UI confusion, only one topic can have an expanded menu at a time. // To avoid UI confusion, only one topic can have an expanded menu at a time.
@ -255,21 +255,21 @@ fun NewsResourceTopics(
val contentDescription = if (followableTopic.isFollowed) { val contentDescription = if (followableTopic.isFollowed) {
stringResource( stringResource(
R.string.topic_chip_content_description_when_followed, R.string.topic_chip_content_description_when_followed,
followableTopic.topic.name followableTopic.topic.name,
) )
} else { } else {
stringResource( stringResource(
R.string.topic_chip_content_description_when_not_followed, R.string.topic_chip_content_description_when_not_followed,
followableTopic.topic.name followableTopic.topic.name,
) )
} }
Text( Text(
text = followableTopic.topic.name.uppercase(Locale.getDefault()), text = followableTopic.topic.name.uppercase(Locale.getDefault()),
modifier = Modifier.semantics { modifier = Modifier.semantics {
this.contentDescription = contentDescription this.contentDescription = contentDescription
} },
) )
} },
) )
} }
} }
@ -304,7 +304,7 @@ private fun ExpandedNewsResourcePreview() {
userNewsResource = previewUserNewsResources[0], userNewsResource = previewUserNewsResources[0],
isBookmarked = true, isBookmarked = true,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {} onClick = {},
) )
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save