Merge branch 'github/main'

pull/591/head^2
Automerger 2 years ago
commit 80cb70ae2d

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

@ -29,10 +29,6 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity
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.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
@ -40,6 +36,10 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
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.
@ -57,7 +57,8 @@ class NavigationTest {
* 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.
*/
@BindValue @get:Rule(order = 1)
@BindValue
@get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/**
@ -165,7 +166,6 @@ class NavigationTest {
@Test
fun topLevelDestinations_showTopBarWithTitle() {
composeTestRule.apply {
// Verify that the top bar contains the app name on the first screen.
onNodeWithText(appName).assertExists()
@ -207,7 +207,6 @@ class NavigationTest {
@Test
fun whenSettingsDialogDismissed_previousScreenIsDisplayed() {
composeTestRule.apply {
// Navigate to the saved screen, open the settings dialog, then close it.
onNodeWithText(saved).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.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import javax.inject.Inject
/**
* 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
* 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()
/**
@ -77,9 +78,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -96,9 +97,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -115,9 +116,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -134,9 +135,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -153,9 +154,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -172,9 +173,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -191,9 +192,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -210,9 +211,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight)
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
}
@ -229,9 +230,9 @@ class NavigationUiTest {
BoxWithConstraints {
NiaApp(
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.testing.TestNavHostController
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.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* Tests [NiaAppState].
@ -70,7 +70,7 @@ class NiaAppStateTest {
windowSizeClass = getCompactWindowClass(),
navController = navController,
networkMonitor = networkMonitor,
coroutineScope = backgroundScope
coroutineScope = backgroundScope,
)
}
@ -91,7 +91,7 @@ class NiaAppStateTest {
composeTestRule.setContent {
state = rememberNiaAppState(
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor
networkMonitor = networkMonitor,
)
}
@ -108,7 +108,7 @@ class NiaAppStateTest {
windowSizeClass = getCompactWindowClass(),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope
coroutineScope = backgroundScope,
)
}
@ -123,7 +123,7 @@ class NiaAppStateTest {
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope
coroutineScope = backgroundScope,
)
}
@ -133,13 +133,12 @@ class NiaAppStateTest {
@Test
fun niaAppState_showNavRail_large() = runTest {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope
coroutineScope = backgroundScope,
)
}
@ -149,13 +148,12 @@ class NiaAppStateTest {
@Test
fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) {
composeTestRule.setContent {
state = NiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
navController = NavHostController(LocalContext.current),
networkMonitor = networkMonitor,
coroutineScope = backgroundScope
coroutineScope = backgroundScope,
)
}
@ -163,7 +161,7 @@ class NiaAppStateTest {
networkMonitor.setConnected(false)
assertEquals(
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.ui.NiaApp
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint
@ -107,7 +107,7 @@ class MainActivity : ComponentActivity() {
NiaTheme(
darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(uiState),
disableDynamicTheming = shouldDisableDynamicTheming(uiState)
disableDynamicTheming = shouldDisableDynamicTheming(uiState),
) {
NiaApp(
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.model.data.UserData
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class MainActivityViewModel @Inject constructor(
userDataRepository: UserDataRepository
userDataRepository: UserDataRepository,
) : ViewModel() {
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map {
Success(it)
}.stateIn(
scope = viewModelScope,
initialValue = Loading,
started = SharingStarted.WhileSubscribed(5_000)
started = SharingStarted.WhileSubscribed(5_000),
)
}

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

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

@ -34,24 +34,24 @@ enum class TopLevelDestination(
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int,
val titleTextId: Int
val titleTextId: Int,
) {
FOR_YOU(
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = forYouR.string.for_you,
titleTextId = R.string.app_name
titleTextId = R.string.app_name,
),
BOOKMARKS(
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = bookmarksR.string.saved,
titleTextId = bookmarksR.string.saved
titleTextId = bookmarksR.string.saved,
),
INTERESTS(
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
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.theme.GradientColors
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.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalLayoutApi::class,
ExperimentalComposeUiApi::class,
ExperimentalLifecycleComposeApi::class
ExperimentalLifecycleComposeApi::class,
)
@Composable
fun NiaApp(
@ -85,7 +85,7 @@ fun NiaApp(
networkMonitor: NetworkMonitor,
appState: NiaAppState = rememberNiaAppState(
networkMonitor = networkMonitor,
windowSizeClass = windowSizeClass
windowSizeClass = windowSizeClass,
),
) {
val shouldShowGradientBackground =
@ -106,15 +106,17 @@ fun NiaApp(
// If user is not connected to the internet show a snack bar to inform them.
val notConnectedMessage = stringResource(R.string.not_connected)
LaunchedEffect(isOffline) {
if (isOffline) snackbarHostState.showSnackbar(
message = notConnectedMessage,
duration = Indefinite
)
if (isOffline) {
snackbarHostState.showSnackbar(
message = notConnectedMessage,
duration = Indefinite,
)
}
}
if (appState.shouldShowSettingsDialog) {
SettingsDialog(
onDismiss = { appState.setShowSettingsDialog(false) }
onDismiss = { appState.setShowSettingsDialog(false) },
)
}
@ -132,10 +134,10 @@ fun NiaApp(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination,
modifier = Modifier.testTag("NiaBottomBar")
modifier = Modifier.testTag("NiaBottomBar"),
)
}
}
},
) { padding ->
Row(
Modifier
@ -144,9 +146,9 @@ fun NiaApp(
.consumedWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
)
)
WindowInsetsSides.Horizontal,
),
),
) {
if (appState.shouldShowNavRail) {
NiaNavRail(
@ -155,7 +157,7 @@ fun NiaApp(
currentDestination = appState.currentDestination,
modifier = Modifier
.testTag("NiaNavRail")
.safeDrawingPadding()
.safeDrawingPadding(),
)
}
@ -167,18 +169,18 @@ fun NiaApp(
titleRes = destination.titleTextId,
actionIcon = NiaIcons.Settings,
actionIconContentDescription = stringResource(
id = settingsR.string.top_app_bar_action_icon_description
id = settingsR.string.top_app_bar_action_icon_description,
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
containerColor = Color.Transparent,
),
onActionClick = { appState.setShowSettingsDialog(true) }
onActionClick = { appState.setShowSettingsDialog(true) },
)
}
NiaNavHost(
navController = appState.navController,
onBackClick = appState::onBackClick
onBackClick = appState::onBackClick,
)
}
@ -212,15 +214,15 @@ private fun NiaNavRail(
when (icon) {
is ImageVectorIcon -> Icon(
imageVector = icon.imageVector,
contentDescription = null
contentDescription = null,
)
is DrawableResourceIcon -> Icon(
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>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
NiaNavigationBar(
modifier = modifier
modifier = modifier,
) {
destinations.forEach { destination ->
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
@ -250,16 +252,16 @@ private fun NiaBottomBar(
when (icon) {
is ImageVectorIcon -> Icon(
imageVector = icon.imageVector,
contentDescription = null
contentDescription = null,
)
is DrawableResourceIcon -> Icon(
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,
networkMonitor: NetworkMonitor,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
navController: NavHostController = rememberNavController()
navController: NavHostController = rememberNavController(),
): NiaAppState {
NavigationTrackingSideEffect(navController)
return remember(navController, coroutineScope, windowSizeClass, networkMonitor) {
@ -98,7 +98,7 @@ class NiaAppState(
.stateIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
initialValue = false,
)
/**

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

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

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

@ -66,7 +66,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
@Test
fun startupBaselineProfileDisabled() = startup(
CompilationMode.Partial(baselineProfileMode = Disable, warmupIterations = 1)
CompilationMode.Partial(baselineProfileMode = Disable, warmupIterations = 1),
)
@Test
@ -83,7 +83,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
startupMode = startupMode,
setupBlock = {
pressHome()
}
},
) {
startActivityAndWait()
// 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)
enum class NiaDispatchers {
IO
IO,
}

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

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

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

@ -19,9 +19,9 @@ package com.google.samples.apps.nowinandroid.core.data
import android.util.Log
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlin.coroutines.cancellation.CancellationException
/**
* 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(
"suspendRunCatching",
"Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
exception
exception,
)
Result.failure(exception)
}
@ -116,10 +116,10 @@ fun <T1, T2, T3, T4, T5, T6, R> combine(
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
transform: suspend (T1, T2, T3, T4, T5, T6) -> R
transform: suspend (T1, T2, T3, T4, T5, T6) -> R,
): Flow<R> = combine(
combine(flow, flow2, flow3, ::Triple),
combine(flow4, flow5, flow6, ::Triple)
combine(flow4, flow5, flow6, ::Triple),
) { t1, t2 ->
transform(
t1.first,
@ -127,6 +127,6 @@ fun <T1, T2, T3, T4, T5, T6, R> combine(
t1.third,
t2.first,
t2.second,
t2.third
t2.third,
)
}

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

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

@ -25,5 +25,5 @@ fun NetworkTopic.asEntity() = TopicEntity(
shortDescription = shortDescription,
longDescription = longDescription,
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.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
* Disk storage backed implementation of the [NewsRepository].
@ -49,9 +49,9 @@ class OfflineFirstNewsRepository @Inject constructor(
.map { it.map(PopulatedNewsResource::asExternalModel) }
override fun getNewsResources(
filterTopicIds: Set<String>
filterTopicIds: Set<String>,
): Flow<List<NewsResource>> = newsResourceDao.getNewsResources(
filterTopicIds = filterTopicIds
filterTopicIds = filterTopicIds,
)
.map { it.map(PopulatedNewsResource::asExternalModel) }
@ -74,18 +74,18 @@ class OfflineFirstNewsRepository @Inject constructor(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
.distinctBy(TopicEntity::id),
)
newsResourceDao.upsertNewsResources(
newsResourceEntities = networkNewsResources
.map(NetworkNewsResource::asEntity)
.map(NetworkNewsResource::asEntity),
)
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences = networkNewsResources
.map(NetworkNewsResource::topicCrossReferences)
.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.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
* Disk storage backed implementation of the [TopicsRepository].
@ -59,8 +59,8 @@ class OfflineFirstTopicsRepository @Inject constructor(
modelUpdater = { changedIds ->
val networkTopics = network.getTopics(ids = changedIds)
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.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class OfflineFirstUserDataRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource
private val niaPreferencesDataSource: NiaPreferencesDataSource,
) : UserDataRepository {
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.fake.FakeNiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
/**
* 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(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val datasource: FakeNiaNetworkDataSource
private val datasource: FakeNiaNetworkDataSource,
) : NewsRepository {
override fun getNewsResources(): Flow<List<NewsResource>> =
@ -48,7 +48,7 @@ class FakeNewsRepository @Inject constructor(
emit(
datasource.getNewsResources()
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel)
.map(NewsResourceEntity::asExternalModel),
)
}.flowOn(ioDispatcher)
@ -61,7 +61,7 @@ class FakeNewsRepository @Inject constructor(
.getNewsResources()
.filter { it.topics.intersect(filterTopicIds).isNotEmpty() }
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel)
.map(NewsResourceEntity::asExternalModel),
)
}.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.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
* 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(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val datasource: FakeNiaNetworkDataSource
private val datasource: FakeNiaNetworkDataSource,
) : TopicsRepository {
override fun getTopics(): Flow<List<Topic>> = flow {
emit(
@ -49,9 +49,9 @@ class FakeTopicsRepository @Inject constructor(
shortDescription = it.shortDescription,
longDescription = it.longDescription,
url = it.url,
imageUrl = it.imageUrl
imageUrl = it.imageUrl,
)
}
},
)
}.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.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/**
* 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 androidx.core.content.getSystemService
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import javax.inject.Inject
class ConnectivityManagerNetworkMonitor @Inject constructor(
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
) : NetworkMonitor {
override val isOnline: Flow<Boolean> = callbackFlow {
val connectivityManager = context.getSystemService<ConnectivityManager>()
@ -54,7 +54,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
networkCapabilities: NetworkCapabilities,
) {
channel.trySend(connectivityManager.isCurrentlyConnected())
}
@ -64,7 +64,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build(),
callback
callback,
)
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.NetworkNewsResourceExpanded
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlin.test.assertEquals
import kotlinx.datetime.Instant
import org.junit.Test
import kotlin.test.assertEquals
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.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
class OfflineFirstNewsRepositoryTest {
@ -65,8 +65,8 @@ class OfflineFirstNewsRepositoryTest {
network = TestNiaNetworkDataSource()
synchronizer = TestSynchronizer(
NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore()
)
tmpFolder.testUserPreferencesDataStore(),
),
)
subject = OfflineFirstNewsRepository(
@ -84,7 +84,7 @@ class OfflineFirstNewsRepositoryTest {
.first()
.map(PopulatedNewsResource::asExternalModel),
subject.getNewsResources()
.first()
.first(),
)
}
@ -100,7 +100,7 @@ class OfflineFirstNewsRepositoryTest {
subject.getNewsResources(
filterTopicIds = filteredInterestsIds,
)
.first()
.first(),
)
assertEquals(
@ -108,7 +108,7 @@ class OfflineFirstNewsRepositoryTest {
subject.getNewsResources(
filterTopicIds = nonPresentInterestsIds,
)
.first()
.first(),
)
}
@ -127,13 +127,13 @@ class OfflineFirstNewsRepositoryTest {
assertEquals(
newsResourcesFromNetwork.map(NewsResource::id),
newsResourcesFromDb.map(NewsResource::id)
newsResourcesFromDb.map(NewsResource::id),
)
// After sync version should be updated
assertEquals(
network.latestChangeListVersion(CollectionType.NewsResources),
synchronizer.getChangeListVersions().newsResourceVersion
synchronizer.getChangeListVersions().newsResourceVersion,
)
}
@ -155,7 +155,7 @@ class OfflineFirstNewsRepositoryTest {
network.editCollection(
collectionType = CollectionType.NewsResources,
id = it,
isDelete = true
isDelete = true,
)
}
@ -168,13 +168,13 @@ class OfflineFirstNewsRepositoryTest {
// Assert that items marked deleted on the network have been deleted locally
assertEquals(
newsResourcesFromNetwork.map(NewsResource::id) - deletedItems,
newsResourcesFromDb.map(NewsResource::id)
newsResourcesFromDb.map(NewsResource::id),
)
// After sync version should be updated
assertEquals(
network.latestChangeListVersion(CollectionType.NewsResources),
synchronizer.getChangeListVersions().newsResourceVersion
synchronizer.getChangeListVersions().newsResourceVersion,
)
}
@ -190,7 +190,7 @@ class OfflineFirstNewsRepositoryTest {
val changeList = network.changeListsAfter(
CollectionType.NewsResources,
version = 7
version = 7,
)
val changeListIds = changeList
.map(NetworkChangeList::id)
@ -207,13 +207,13 @@ class OfflineFirstNewsRepositoryTest {
assertEquals(
newsResourcesFromNetwork.map(NewsResource::id),
newsResourcesFromDb.map(NewsResource::id)
newsResourcesFromDb.map(NewsResource::id),
)
// After sync version should be updated
assertEquals(
changeList.last().changeListVersion,
synchronizer.getChangeListVersions().newsResourceVersion
synchronizer.getChangeListVersions().newsResourceVersion,
)
}
@ -228,7 +228,7 @@ class OfflineFirstNewsRepositoryTest {
.flatten()
.distinctBy(TopicEntity::id),
topicDao.getTopicEntities()
.first()
.first(),
)
}
@ -242,7 +242,7 @@ class OfflineFirstNewsRepositoryTest {
.map(NetworkNewsResource::topicCrossReferences)
.distinct()
.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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
class OfflineFirstTopicsRepositoryTest {
@ -56,13 +56,13 @@ class OfflineFirstTopicsRepositoryTest {
topicDao = TestTopicDao()
network = TestNiaNetworkDataSource()
niaPreferences = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore()
tmpFolder.testUserPreferencesDataStore(),
)
synchronizer = TestSynchronizer(niaPreferences)
subject = OfflineFirstTopicsRepository(
topicDao = topicDao,
network = network
network = network,
)
}
@ -74,7 +74,7 @@ class OfflineFirstTopicsRepositoryTest {
.first()
.map(TopicEntity::asExternalModel),
subject.getTopics()
.first()
.first(),
)
}
@ -91,13 +91,13 @@ class OfflineFirstTopicsRepositoryTest {
assertEquals(
networkTopics.map(TopicEntity::id),
dbTopics.map(TopicEntity::id)
dbTopics.map(TopicEntity::id),
)
// After sync version should be updated
assertEquals(
network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion
synchronizer.getChangeListVersions().topicVersion,
)
}
@ -121,13 +121,13 @@ class OfflineFirstTopicsRepositoryTest {
assertEquals(
networkTopics.map(TopicEntity::id),
dbTopics.map(TopicEntity::id)
dbTopics.map(TopicEntity::id),
)
// After sync version should be updated
assertEquals(
network.latestChangeListVersion(CollectionType.Topics),
synchronizer.getChangeListVersions().topicVersion
synchronizer.getChangeListVersions().topicVersion,
)
}
@ -149,7 +149,7 @@ class OfflineFirstTopicsRepositoryTest {
network.editCollection(
collectionType = CollectionType.Topics,
id = it,
isDelete = true
isDelete = true,
)
}
@ -162,13 +162,13 @@ class OfflineFirstTopicsRepositoryTest {
// Assert that items marked deleted on the network have been deleted locally
assertEquals(
networkTopics.map(Topic::id) - deletedItems,
dbTopics.map(Topic::id)
dbTopics.map(Topic::id),
)
// After sync version should be updated
assertEquals(
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.ThemeBrand
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.map
import kotlinx.coroutines.test.runTest
@ -31,6 +28,9 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class OfflineFirstUserDataRepositoryTest {
private lateinit var subject: OfflineFirstUserDataRepository
@ -43,11 +43,11 @@ class OfflineFirstUserDataRepositoryTest {
@Before
fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore()
tmpFolder.testUserPreferencesDataStore(),
)
subject = OfflineFirstUserDataRepository(
niaPreferencesDataSource = niaPreferencesDataSource
niaPreferencesDataSource = niaPreferencesDataSource,
)
}
@ -61,9 +61,9 @@ class OfflineFirstUserDataRepositoryTest {
themeBrand = ThemeBrand.DEFAULT,
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
useDynamicColor = false,
shouldHideOnboarding = false
shouldHideOnboarding = false,
),
subject.userData.first()
subject.userData.first(),
)
}
@ -76,7 +76,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0"),
subject.userData
.map { it.followedTopics }
.first()
.first(),
)
subject.toggleFollowedTopicId(followedTopicId = "1", followed = true)
@ -85,7 +85,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0", "1"),
subject.userData
.map { it.followedTopics }
.first()
.first(),
)
assertEquals(
@ -94,7 +94,7 @@ class OfflineFirstUserDataRepositoryTest {
.first(),
subject.userData
.map { it.followedTopics }
.first()
.first(),
)
}
@ -107,7 +107,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("1", "2"),
subject.userData
.map { it.followedTopics }
.first()
.first(),
)
assertEquals(
@ -116,7 +116,7 @@ class OfflineFirstUserDataRepositoryTest {
.first(),
subject.userData
.map { it.followedTopics }
.first()
.first(),
)
}
@ -129,7 +129,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0"),
subject.userData
.map { it.bookmarkedNewsResources }
.first()
.first(),
)
subject.updateNewsResourceBookmark(newsResourceId = "1", bookmarked = true)
@ -138,7 +138,7 @@ class OfflineFirstUserDataRepositoryTest {
setOf("0", "1"),
subject.userData
.map { it.bookmarkedNewsResources }
.first()
.first(),
)
assertEquals(
@ -147,7 +147,7 @@ class OfflineFirstUserDataRepositoryTest {
.first(),
subject.userData
.map { it.bookmarkedNewsResources }
.first()
.first(),
)
}
@ -160,14 +160,14 @@ class OfflineFirstUserDataRepositoryTest {
ThemeBrand.ANDROID,
subject.userData
.map { it.themeBrand }
.first()
.first(),
)
assertEquals(
ThemeBrand.ANDROID,
niaPreferencesDataSource
.userData
.map { it.themeBrand }
.first()
.first(),
)
}
@ -180,14 +180,14 @@ class OfflineFirstUserDataRepositoryTest {
true,
subject.userData
.map { it.useDynamicColor }
.first()
.first(),
)
assertEquals(
true,
niaPreferencesDataSource
.userData
.map { it.useDynamicColor }
.first()
.first(),
)
}
@ -200,14 +200,14 @@ class OfflineFirstUserDataRepositoryTest {
DarkThemeConfig.DARK,
subject.userData
.map { it.darkThemeConfig }
.first()
.first(),
)
assertEquals(
DarkThemeConfig.DARK,
niaPreferencesDataSource
.userData
.map { it.darkThemeConfig }
.first()
.first(),
)
}

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

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

@ -27,7 +27,7 @@ import kotlinx.serialization.json.Json
enum class CollectionType {
Topics,
NewsResources
NewsResources,
}
/**
@ -37,7 +37,7 @@ class TestNiaNetworkDataSource : NiaNetworkDataSource {
private val source = FakeNiaNetworkDataSource(
UnconfinedTestDispatcher(),
Json { ignoreUnknownKeys = true }
Json { ignoreUnknownKeys = true },
)
private val allTopics = runBlocking { source.getTopics() }
@ -54,13 +54,13 @@ class TestNiaNetworkDataSource : NiaNetworkDataSource {
override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> =
allTopics.matchIds(
ids = ids,
idGetter = NetworkTopic::id
idGetter = NetworkTopic::id,
)
override suspend fun getNewsResources(ids: List<String>?): List<NetworkNewsResource> =
allNewsResources.matchIds(
ids = ids,
idGetter = NetworkNewsResource::id
idGetter = NetworkNewsResource::id,
)
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(
ids: List<String>?,
idGetter: (T) -> String
idGetter: (T) -> String,
) = when (ids) {
null -> this
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
*/
private fun <T> List<T>.mapToChangeList(
idGetter: (T) -> String
idGetter: (T) -> String,
) = mapIndexed { index, item ->
NetworkChangeList(
id = idGetter(item),

@ -37,8 +37,8 @@ class TestTopicDao : TopicDao {
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
)
),
),
)
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.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlin.test.assertEquals
import kotlinx.datetime.Instant
import org.junit.Test
import kotlin.test.assertEquals
class PopulatedNewsResourceKtTest {
@Test
@ -44,7 +44,7 @@ class PopulatedNewsResourceKtTest {
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
),
),
)
val newsResource = populatedNewsResource.asExternalModel()
@ -66,10 +66,10 @@ class PopulatedNewsResourceKtTest {
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
)
),
),
),
newsResource
newsResource,
)
}
}

@ -17,8 +17,8 @@
package com.google.samples.apps.nowinandroid.core.database.util
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlin.test.assertEquals
import org.junit.Test
import kotlin.test.assertEquals
class NewsResourceTypeConverterTest {
@ -26,7 +26,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_video() {
assertEquals(
NewsResourceType.Video,
NewsResourceTypeConverter().stringToNewsResourceType("Video 📺")
NewsResourceTypeConverter().stringToNewsResourceType("Video 📺"),
)
}
@ -34,7 +34,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_article() {
assertEquals(
NewsResourceType.Article,
NewsResourceTypeConverter().stringToNewsResourceType("Article 📚")
NewsResourceTypeConverter().stringToNewsResourceType("Article 📚"),
)
}
@ -42,7 +42,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_api_change() {
assertEquals(
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() {
assertEquals(
NewsResourceType.Codelab,
NewsResourceTypeConverter().stringToNewsResourceType("Codelab")
NewsResourceTypeConverter().stringToNewsResourceType("Codelab"),
)
}
@ -58,7 +58,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_podcast() {
assertEquals(
NewsResourceType.Podcast,
NewsResourceTypeConverter().stringToNewsResourceType("Podcast 🎙")
NewsResourceTypeConverter().stringToNewsResourceType("Podcast 🎙"),
)
}
@ -66,7 +66,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_docs() {
assertEquals(
NewsResourceType.Docs,
NewsResourceTypeConverter().stringToNewsResourceType("Docs 📑")
NewsResourceTypeConverter().stringToNewsResourceType("Docs 📑"),
)
}
@ -74,7 +74,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_event() {
assertEquals(
NewsResourceType.Event,
NewsResourceTypeConverter().stringToNewsResourceType("Event 📆")
NewsResourceTypeConverter().stringToNewsResourceType("Event 📆"),
)
}
@ -82,7 +82,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_dac() {
assertEquals(
NewsResourceType.DAC,
NewsResourceTypeConverter().stringToNewsResourceType("DAC")
NewsResourceTypeConverter().stringToNewsResourceType("DAC"),
)
}
@ -90,7 +90,7 @@ class NewsResourceTypeConverterTest {
fun test_room_news_resource_type_converter_for_umm() {
assertEquals(
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.asExternalModel
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
class NewsResourceDaoTest {
@ -43,7 +43,7 @@ class NewsResourceDaoTest {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context,
NiaDatabase::class.java
NiaDatabase::class.java,
).build()
newsResourceDao = db.newsResourceDao()
topicDao = db.topicDao()
@ -70,7 +70,7 @@ class NewsResourceDaoTest {
),
)
newsResourceDao.upsertNewsResources(
newsResourceEntities
newsResourceEntities,
)
val savedNewsResourceEntities = newsResourceDao.getNewsResources()
@ -80,7 +80,7 @@ class NewsResourceDaoTest {
listOf(3L, 2L, 1L, 0L),
savedNewsResourceEntities.map {
it.asExternalModel().publishDate.toEpochMilliseconds()
}
},
)
}
@ -89,11 +89,11 @@ class NewsResourceDaoTest {
val topicEntities = listOf(
testTopicEntity(
id = "1",
name = "1"
name = "1",
),
testTopicEntity(
id = "2",
name = "2"
name = "2",
),
)
val newsResourceEntities = listOf(
@ -117,18 +117,18 @@ class NewsResourceDaoTest {
val newsResourceTopicCrossRefEntities = topicEntities.mapIndexed { index, topicEntity ->
NewsResourceTopicCrossRef(
newsResourceId = index.toString(),
topicId = topicEntity.id
topicId = topicEntity.id,
)
}
topicDao.insertOrIgnoreTopics(
topicEntities = topicEntities
topicEntities = topicEntities,
)
newsResourceDao.upsertNewsResources(
newsResourceEntities
newsResourceEntities,
)
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossRefEntities
newsResourceTopicCrossRefEntities,
)
val filteredNewsResources = newsResourceDao.getNewsResources(
@ -139,7 +139,7 @@ class NewsResourceDaoTest {
assertEquals(
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 }
newsResourceDao.deleteNewsResources(
toDelete.map(NewsResourceEntity::id)
toDelete.map(NewsResourceEntity::id),
)
assertEquals(
@ -177,26 +177,26 @@ class NewsResourceDaoTest {
.toSet(),
newsResourceDao.getNewsResources().first()
.map { it.entity.id }
.toSet()
.toSet(),
)
}
}
private fun testTopicEntity(
id: String = "0",
name: String
name: String,
) = TopicEntity(
id = id,
name = name,
shortDescription = "",
longDescription = "",
url = "",
imageUrl = ""
imageUrl = "",
)
private fun testNewsResource(
id: String = "0",
millisSinceEpoch: Long = 0
millisSinceEpoch: Long = 0,
) = NewsResourceEntity(
id = id,
title = "",

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

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

@ -46,7 +46,7 @@ import com.google.samples.apps.nowinandroid.core.database.util.NewsResourceTypeC
AutoMigration(from = 8, to = 9),
AutoMigration(from = 9, to = 10),
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,
)

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

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

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

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

@ -34,9 +34,9 @@ data class PopulatedNewsResource(
value = NewsResourceTopicCrossRef::class,
parentColumn = "news_resource_id",
entityColumn = "topic_id",
)
),
)
val topics: List<TopicEntity>
val topics: List<TopicEntity>,
)
fun PopulatedNewsResource.asExternalModel() = NewsResource(
@ -47,5 +47,5 @@ fun PopulatedNewsResource.asExternalModel() = NewsResource(
headerImageUrl = entity.headerImageUrl,
publishDate = entity.publishDate,
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.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton
import org.junit.rules.TemporaryFolder
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [DataStoreModule::class]
replaces = [DataStoreModule::class],
)
object TestDataStoreModule {
@ -39,13 +39,13 @@ object TestDataStoreModule {
@Singleton
fun providesUserPreferencesDataStore(
userPreferencesSerializer: UserPreferencesSerializer,
tmpFolder: TemporaryFolder
tmpFolder: TemporaryFolder,
): DataStore<UserPreferences> =
tmpFolder.testUserPreferencesDataStore(userPreferencesSerializer)
}
fun TemporaryFolder.testUserPreferencesDataStore(
userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer()
userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer(),
) = DataStoreFactory.create(
serializer = userPreferencesSerializer,
) {

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

@ -30,21 +30,21 @@ object ListToMapMigration : DataMigration<UserPreferences> {
// Migrate topic id lists
followedTopicIds.clear()
followedTopicIds.putAll(
currentData.deprecatedFollowedTopicIdsList.associateWith { true }
currentData.deprecatedFollowedTopicIdsList.associateWith { true },
)
deprecatedFollowedTopicIds.clear()
// Migrate author ids
followedAuthorIds.clear()
followedAuthorIds.putAll(
currentData.deprecatedFollowedAuthorIdsList.associateWith { true }
currentData.deprecatedFollowedAuthorIdsList.associateWith { true },
)
deprecatedFollowedAuthorIds.clear()
// Migrate bookmarks
bookmarkedNewsResourceIds.clear()
bookmarkedNewsResourceIds.putAll(
currentData.deprecatedBookmarkedNewsResourceIdsList.associateWith { true }
currentData.deprecatedBookmarkedNewsResourceIdsList.associateWith { true },
)
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.ThemeBrand
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.map
import java.io.IOException
import javax.inject.Inject
class NiaPreferencesDataSource @Inject constructor(
private val userPreferences: DataStore<UserPreferences>
private val userPreferences: DataStore<UserPreferences>,
) {
val userData = userPreferences.data
.map {
@ -38,21 +38,23 @@ class NiaPreferencesDataSource @Inject constructor(
null,
ThemeBrandProto.THEME_BRAND_UNSPECIFIED,
ThemeBrandProto.UNRECOGNIZED,
ThemeBrandProto.THEME_BRAND_DEFAULT -> ThemeBrand.DEFAULT
ThemeBrandProto.THEME_BRAND_DEFAULT,
-> ThemeBrand.DEFAULT
ThemeBrandProto.THEME_BRAND_ANDROID -> ThemeBrand.ANDROID
},
darkThemeConfig = when (it.darkThemeConfig) {
null,
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
DarkThemeConfigProto.UNRECOGNIZED,
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM ->
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM,
->
DarkThemeConfig.FOLLOW_SYSTEM
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT ->
DarkThemeConfig.LIGHT
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK
},
useDynamicColor = it.useDynamicColor,
shouldHideOnboarding = it.shouldHideOnboarding
shouldHideOnboarding = it.shouldHideOnboarding,
)
}
@ -153,8 +155,8 @@ class NiaPreferencesDataSource @Inject constructor(
val updatedChangeListVersions = update(
ChangeListVersions(
topicVersion = currentPreferences.topicChangeListVersion,
newsResourceVersion = currentPreferences.newsResourceChangeListVersion
)
newsResourceVersion = currentPreferences.newsResourceChangeListVersion,
),
)
currentPreferences.copy {
@ -177,7 +179,6 @@ class NiaPreferencesDataSource @Inject constructor(
}
private fun UserPreferencesKt.Dsl.updateShouldHideOnboardingIfNecessary() {
if (followedTopicIds.isEmpty() && followedAuthorIds.isEmpty()) {
shouldHideOnboarding = false
}

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

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

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

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

@ -17,11 +17,11 @@
package com.google.samples.apps.nowinandroid.core.datastore
import androidx.datastore.core.CorruptionException
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
class UserPreferencesSerializerTest {
private val userPreferencesSerializer = UserPreferencesSerializer()
@ -32,7 +32,7 @@ class UserPreferencesSerializerTest {
userPreferences {
// Default value
},
userPreferencesSerializer.defaultValue
userPreferencesSerializer.defaultValue,
)
}
@ -53,7 +53,7 @@ class UserPreferencesSerializerTest {
assertEquals(
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.NiaTheme
import com.google.samples.apps.nowinandroid.core.designsystem.theme.TintTheme
import kotlin.test.assertEquals
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
/**
* Tests [NiaTheme] using different combinations of the theme mode parameters:
@ -64,7 +64,7 @@ class ThemeTest {
NiaTheme(
darkTheme = false,
disableDynamicTheming = true,
androidTheme = false
androidTheme = false,
) {
val colorScheme = LightDefaultColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -84,7 +84,7 @@ class ThemeTest {
NiaTheme(
darkTheme = true,
disableDynamicTheming = true,
androidTheme = false
androidTheme = false,
) {
val colorScheme = DarkDefaultColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -104,7 +104,7 @@ class ThemeTest {
NiaTheme(
darkTheme = false,
disableDynamicTheming = false,
androidTheme = false
androidTheme = false,
) {
val colorScheme = dynamicLightColorSchemeWithFallback()
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -124,7 +124,7 @@ class ThemeTest {
NiaTheme(
darkTheme = true,
disableDynamicTheming = false,
androidTheme = false
androidTheme = false,
) {
val colorScheme = dynamicDarkColorSchemeWithFallback()
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -144,7 +144,7 @@ class ThemeTest {
NiaTheme(
darkTheme = false,
disableDynamicTheming = true,
androidTheme = true
androidTheme = true,
) {
val colorScheme = LightAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -164,7 +164,7 @@ class ThemeTest {
NiaTheme(
darkTheme = true,
disableDynamicTheming = true,
androidTheme = true
androidTheme = true,
) {
val colorScheme = DarkAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -184,7 +184,7 @@ class ThemeTest {
NiaTheme(
darkTheme = false,
disableDynamicTheming = false,
androidTheme = true
androidTheme = true,
) {
val colorScheme = LightAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -204,7 +204,7 @@ class ThemeTest {
NiaTheme(
darkTheme = true,
disableDynamicTheming = false,
androidTheme = true
androidTheme = true,
) {
val colorScheme = DarkAndroidColorScheme
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
@ -244,7 +244,7 @@ class ThemeTest {
return GradientColors(
top = colorScheme.inverseOnSurface,
bottom = colorScheme.primaryContainer,
container = colorScheme.surface
container = colorScheme.surface,
)
}
@ -259,7 +259,7 @@ class ThemeTest {
private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme {
return BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp
tonalElevation = 2.dp,
)
}
@ -280,7 +280,7 @@ class ThemeTest {
*/
private fun assertColorSchemesEqual(
expectedColorScheme: ColorScheme,
actualColorScheme: ColorScheme
actualColorScheme: ColorScheme,
) {
assertEquals(expectedColorScheme.primary, actualColorScheme.primary)
assertEquals(expectedColorScheme.onPrimary, actualColorScheme.onPrimary)
@ -291,7 +291,7 @@ class ThemeTest {
assertEquals(expectedColorScheme.secondaryContainer, actualColorScheme.secondaryContainer)
assertEquals(
expectedColorScheme.onSecondaryContainer,
actualColorScheme.onSecondaryContainer
actualColorScheme.onSecondaryContainer,
)
assertEquals(expectedColorScheme.tertiary, actualColorScheme.tertiary)
assertEquals(expectedColorScheme.onTertiary, actualColorScheme.onTertiary)

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

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

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

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

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

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

@ -53,7 +53,7 @@ import kotlinx.coroutines.launch
@Composable
fun NiaLoadingWheel(
contentDesc: String,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
val infiniteTransition = rememberInfiniteTransition()
@ -68,8 +68,8 @@ fun NiaLoadingWheel(
animationSpec = tween(
durationMillis = 100,
easing = FastOutSlowInEasing,
delayMillis = 40 * index
)
delayMillis = 40 * index,
),
)
}
}
@ -80,8 +80,8 @@ fun NiaLoadingWheel(
initialValue = 0F,
targetValue = 360F,
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
@ -98,8 +98,8 @@ fun NiaLoadingWheel(
baseLineColor at ROTATION_TIME / NUM_OF_LINES with LinearEasing
},
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,
cap = StrokeCap.Round,
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
fun NiaOverlayLoadingWheel(
contentDesc: String,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
Surface(
shape = RoundedCornerShape(60.dp),

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

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

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

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

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

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

@ -29,90 +29,90 @@ internal val NiaTypography = Typography(
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
letterSpacing = (-0.25).sp,
),
displayMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
letterSpacing = 0.sp,
),
displaySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
letterSpacing = 0.sp,
),
headlineLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
letterSpacing = 0.sp,
),
headlineMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
letterSpacing = 0.sp,
),
headlineSmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
letterSpacing = 0.sp,
),
titleLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
letterSpacing = 0.sp,
),
titleMedium = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
lineHeight = 24.sp,
letterSpacing = 0.1.sp
letterSpacing = 0.1.sp,
),
titleSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
letterSpacing = 0.1.sp,
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
letterSpacing = 0.5.sp,
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
letterSpacing = 0.25.sp,
),
bodySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
letterSpacing = 0.4.sp,
),
labelLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
letterSpacing = 0.1.sp,
),
labelMedium = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
letterSpacing = 0.5.sp,
),
labelSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 10.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.NONE
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import javax.inject.Inject
/**
* A use case which obtains a list of topics with their followed state.
*/
class GetFollowableTopicsUseCase @Inject constructor(
private val topicsRepository: TopicsRepository,
private val userDataRepository: UserDataRepository
private val userDataRepository: UserDataRepository,
) {
/**
* 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>> {
return combine(
userDataRepository.userData,
topicsRepository.getTopics()
topicsRepository.getTopics(),
) { userData, topics ->
val followedTopics = topics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in userData.followedTopics
isFollowed = topic.id in userData.followedTopics,
)
}
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.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import javax.inject.Inject
/**
* 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(
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.
@ -42,7 +42,7 @@ class GetUserNewsResourcesUseCase @Inject constructor(
* this is empty the list of news resources will not be filtered.
*/
operator fun invoke(
filterTopicIds: Set<String> = emptySet()
filterTopicIds: Set<String> = emptySet(),
): Flow<List<UserNewsResource>> =
if (filterTopicIds.isEmpty()) {
newsRepository.getNewsResources()
@ -52,7 +52,7 @@ class GetUserNewsResourcesUseCase @Inject constructor(
}
private fun Flow<List<NewsResource>>.mapToUserNewsResources(
userDataStream: Flow<UserData>
userDataStream: Flow<UserData>,
): Flow<List<UserNewsResource>> =
filterNot { it.isEmpty() }
.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
val topic: Topic,
val isFollowed: Boolean
val isFollowed: Boolean,
)
val previewFollowableTopics = listOf(
FollowableTopic(
previewTopics[0],
isFollowed = false
isFollowed = false,
),
FollowableTopic(
previewTopics[1],
isFollowed = true
isFollowed = true,
),
FollowableTopic(
previewTopics[2],
isFollowed = false
)
isFollowed = false,
),
)

@ -42,7 +42,7 @@ data class UserNewsResource internal constructor(
val publishDate: Instant,
val type: NewsResourceType,
val followableTopics: List<FollowableTopic>,
val isSaved: Boolean
val isSaved: Boolean,
) {
constructor(newsResource: NewsResource, userData: UserData) : this(
id = newsResource.id,
@ -55,10 +55,10 @@ data class UserNewsResource internal constructor(
followableTopics = newsResource.topics.map { topic ->
FollowableTopic(
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,
minute = 0,
second = 0,
nanosecond = 0
nanosecond = 0,
).toInstant(TimeZone.UTC),
type = Codelab,
followableTopics = listOf(previewFollowableTopics[1]),
isSaved = true
isSaved = true,
),
UserNewsResource(
id = "2",
@ -98,7 +98,7 @@ val previewUserNewsResources = listOf(
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
followableTopics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]),
isSaved = false
isSaved = false,
),
UserNewsResource(
id = "3",
@ -112,7 +112,7 @@ val previewUserNewsResources = listOf(
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video,
followableTopics = listOf(previewFollowableTopics[2]),
isSaved = false
isSaved = false,
),
UserNewsResource(
id = "4",
@ -124,6 +124,6 @@ val previewUserNewsResources = listOf(
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = Unknown,
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.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class GetFollowableTopicsUseCaseTest {
@ -38,12 +38,11 @@ class GetFollowableTopicsUseCaseTest {
val useCase = GetFollowableTopicsUseCase(
topicsRepository,
userDataRepository
userDataRepository,
)
@Test
fun whenNoParams_followableTopicsAreReturnedWithNoSorting() = runTest {
// Obtain a stream of followable topics.
val followableTopics = useCase()
@ -58,16 +57,15 @@ class GetFollowableTopicsUseCaseTest {
FollowableTopic(testTopics[1], false),
FollowableTopic(testTopics[2], true),
),
followableTopics.first()
followableTopics.first(),
)
}
@Test
fun whenSortOrderIsByName_topicsSortedByNameAreReturned() = runTest {
// Obtain a stream of followable topics, sorted by name.
val followableTopics = useCase(
sortBy = NAME
sortBy = NAME,
)
// Send some test topics and their followed state.
@ -81,7 +79,7 @@ class GetFollowableTopicsUseCaseTest {
.sortedBy { it.name }
.map {
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.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class GetUserNewsResourcesUseCaseTest {
@ -43,7 +43,6 @@ class GetUserNewsResourcesUseCaseTest {
@Test
fun whenNoFilters_allNewsResourcesAreReturned() = runTest {
// Obtain the user news resources stream.
val userNewsResources = useCase()
@ -53,7 +52,7 @@ class GetUserNewsResourcesUseCaseTest {
// Construct the test user data with bookmarks and followed topics.
val userData = emptyUserData.copy(
bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id),
followedTopics = setOf(sampleTopic1.id)
followedTopics = setOf(sampleTopic1.id),
)
userDataRepository.setUserData(userData)
@ -61,13 +60,12 @@ class GetUserNewsResourcesUseCaseTest {
// Check that the correct news resources are returned with their bookmarked state.
assertEquals(
sampleNewsResources.mapToUserNewsResources(userData),
userNewsResources.first()
userNewsResources.first(),
)
}
@Test
fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest {
// Obtain a stream of user news resources for the given topic id.
val userNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id))
@ -80,7 +78,7 @@ class GetUserNewsResourcesUseCaseTest {
sampleNewsResources
.filter { it.topics.contains(sampleTopic1) }
.mapToUserNewsResources(emptyUserData),
userNewsResources.first()
userNewsResources.first(),
)
}
}
@ -115,7 +113,7 @@ private val sampleNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
topics = listOf(sampleTopic1)
topics = listOf(sampleTopic1),
),
NewsResource(
id = "2",
@ -127,7 +125,7 @@ private val sampleNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video,
topics = listOf(sampleTopic1, sampleTopic2)
topics = listOf(sampleTopic1, sampleTopic2),
),
NewsResource(
id = "3",
@ -137,6 +135,6 @@ private val sampleNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/r5JgIyS3t3s/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-08T00:00:00.000Z"),
type = Video,
topics = listOf(sampleTopic2)
topics = listOf(sampleTopic2),
),
)

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

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

@ -23,53 +23,53 @@ enum class NewsResourceType(
val serializedName: String,
val displayText: String,
// TODO: descriptions should probably be string resources
val description: String
val description: String,
) {
Video(
serializedName = "Video 📺",
displayText = "Video 📺",
description = "A video published on YouTube"
description = "A video published on YouTube",
),
APIChange(
serializedName = "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(
serializedName = "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(
serializedName = "Codelab",
displayText = "Codelab",
description = "A new or updated codelab"
description = "A new or updated codelab",
),
Podcast(
serializedName = "Podcast 🎙",
displayText = "Podcast 🎙",
description = "A podcast"
description = "A podcast",
),
Docs(
serializedName = "Docs 📑",
displayText = "Docs 📑",
description = "A new or updated piece of documentation"
description = "A new or updated piece of documentation",
),
Event(
serializedName = "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(
serializedName = "DAC",
displayText = "DAC",
description = "Android version features - Information about features in an Android"
description = "Android version features - Information about features in an Android",
),
Unknown(
serializedName = "Unknown",
displayText = "Unknown",
description = "Unknown"
)
description = "Unknown",
),
}
fun String?.asNewsResourceType() = when (this) {

@ -37,7 +37,7 @@ val previewTopics = listOf(
shortDescription = "News we want everyone to see",
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",
url = ""
url = "",
),
Topic(
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",
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",
url = ""
url = "",
),
Topic(
id = "4",
@ -53,6 +53,6 @@ val previewTopics = listOf(
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.",
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 darkThemeConfig: DarkThemeConfig,
val useDynamicColor: Boolean,
val shouldHideOnboarding: Boolean
val shouldHideOnboarding: Boolean,
)

@ -23,8 +23,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.serialization.json.Json
import javax.inject.Singleton
@Module
@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.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import javax.inject.Inject
/**
* [NiaNetworkDataSource] implementation that provides static news resources to aid development
@ -69,7 +69,7 @@ class FakeNiaNetworkDataSource @Inject constructor(
* [NetworkChangeList.id]
*/
private fun <T> List<T>.mapToChangeList(
idGetter: (T) -> String
idGetter: (T) -> String,
) = mapIndexed { index, item ->
NetworkChangeList(
id = idGetter(item),

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

@ -31,7 +31,7 @@ object NewsResourceTypeSerializer : KSerializer<NewsResourceType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
serialName = "type",
kind = STRING
kind = STRING,
)
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.NetworkTopic
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@ -33,6 +31,8 @@ import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Query
import javax.inject.Inject
import javax.inject.Singleton
/**
* Retrofit API declaration for NIA Network API
@ -66,7 +66,7 @@ private const val NiaBaseUrl = BuildConfig.BACKEND_URL
*/
@Serializable
private data class NetworkResponse<T>(
val data: T
val data: T,
)
/**
@ -74,7 +74,7 @@ private data class NetworkResponse<T>(
*/
@Singleton
class RetrofitNiaNetwork @Inject constructor(
networkJson: Json
networkJson: Json,
) : NiaNetworkDataSource {
private val networkApi = Retrofit.Builder()
@ -85,13 +85,13 @@ class RetrofitNiaNetwork @Inject constructor(
// TODO: Decide logging logic
HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.build()
.build(),
)
.addConverterFactory(
@OptIn(ExperimentalSerializationApi::class)
networkJson.asConverterFactory("application/json".toMediaType())
networkJson.asConverterFactory("application/json".toMediaType()),
)
.build()
.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.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlin.test.assertEquals
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.LocalDateTime
@ -29,6 +28,7 @@ import kotlinx.datetime.toInstant
import kotlinx.serialization.json.Json
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
class FakeNiaNetworkDataSourceTest {
@ -41,7 +41,7 @@ class FakeNiaNetworkDataSourceTest {
subject = FakeNiaNetworkDataSource(
ioDispatcher = testDispatcher,
networkJson = Json { ignoreUnknownKeys = true },
assets = JvmUnitTestFakeAssetManager
assets = JvmUnitTestFakeAssetManager,
)
}
@ -55,10 +55,10 @@ class FakeNiaNetworkDataSourceTest {
shortDescription = "News you'll definitely be interested in",
longDescription = "The latest events and announcements from the world of Android development.",
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 */
subject.getTopics().first()
subject.getTopics().first(),
)
}
@ -79,13 +79,13 @@ class FakeNiaNetworkDataSourceTest {
hour = 23,
minute = 0,
second = 0,
nanosecond = 0
nanosecond = 0,
).toInstant(TimeZone.UTC),
type = Codelab,
topics = listOf("2", "3", "10"),
),
/* 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
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlin.test.assertEquals
import kotlinx.serialization.json.Json
import org.junit.Test
import kotlin.test.assertEquals
class NewsResourceTypeSerializerTest {
@ -27,7 +27,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_video() {
assertEquals(
NewsResourceType.Video,
Json.decodeFromString(NewsResourceTypeSerializer, """"Video 📺"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"Video 📺""""),
)
}
@ -35,7 +35,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_article() {
assertEquals(
NewsResourceType.Article,
Json.decodeFromString(NewsResourceTypeSerializer, """"Article 📚"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"Article 📚""""),
)
}
@ -43,7 +43,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_api_change() {
assertEquals(
NewsResourceType.APIChange,
Json.decodeFromString(NewsResourceTypeSerializer, """"API change"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"API change""""),
)
}
@ -51,7 +51,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_codelab() {
assertEquals(
NewsResourceType.Codelab,
Json.decodeFromString(NewsResourceTypeSerializer, """"Codelab"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"Codelab""""),
)
}
@ -59,7 +59,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_podcast() {
assertEquals(
NewsResourceType.Podcast,
Json.decodeFromString(NewsResourceTypeSerializer, """"Podcast 🎙"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"Podcast 🎙""""),
)
}
@ -67,7 +67,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_docs() {
assertEquals(
NewsResourceType.Docs,
Json.decodeFromString(NewsResourceTypeSerializer, """"Docs 📑"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"Docs 📑""""),
)
}
@ -75,7 +75,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_event() {
assertEquals(
NewsResourceType.Event,
Json.decodeFromString(NewsResourceTypeSerializer, """"Event 📆"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"Event 📆""""),
)
}
@ -83,7 +83,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_dac() {
assertEquals(
NewsResourceType.DAC,
Json.decodeFromString(NewsResourceTypeSerializer, """"DAC"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"DAC""""),
)
}
@ -91,7 +91,7 @@ class NewsResourceTypeSerializerTest {
fun test_news_resource_serializer_unknown() {
assertEquals(
NewsResourceType.Unknown,
Json.decodeFromString(NewsResourceTypeSerializer, """"umm"""")
Json.decodeFromString(NewsResourceTypeSerializer, """"umm""""),
)
}
@ -100,7 +100,7 @@ class NewsResourceTypeSerializerTest {
val json = Json.encodeToString(NewsResourceTypeSerializer, NewsResourceType.Video)
assertEquals(
NewsResourceType.Video,
Json.decodeFromString(NewsResourceTypeSerializer, json)
Json.decodeFromString(NewsResourceTypeSerializer, json),
)
}
}

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

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

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

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

@ -46,7 +46,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsRes
*/
fun LazyGridScope.newsFeed(
feedState: NewsFeedUiState,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
) {
when (feedState) {
NewsFeedUiState.Loading -> Unit
@ -65,9 +65,9 @@ fun LazyGridScope.newsFeed(
onToggleBookmark = {
onNewsResourcesCheckedChanged(
userNewsResource.id,
!userNewsResource.isSaved
!userNewsResource.isSaved,
)
}
},
)
}
}
@ -100,7 +100,7 @@ sealed interface NewsFeedUiState {
/**
* The list of news resources contained in this feed.
*/
val feed: List<UserNewsResource>
val feed: List<UserNewsResource>,
) : NewsFeedUiState
}
@ -111,7 +111,7 @@ private fun NewsFeedLoadingPreview() {
LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) {
newsFeed(
feedState = NewsFeedUiState.Loading,
onNewsResourcesCheckedChanged = { _, _ -> }
onNewsResourcesCheckedChanged = { _, _ -> },
)
}
}
@ -125,9 +125,9 @@ private fun NewsFeedContentPreview() {
LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) {
newsFeed(
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.unit.dp
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.NiaTopicTag
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.model.data.NewsResource
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.format.DateTimeFormatter
import java.util.Locale
import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant
import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR
/**
* [NewsResource] card used on the following screens: For You, Saved
@ -79,7 +79,7 @@ fun NewsResourceCardExpanded(
isBookmarked: Boolean,
onToggleBookmark: () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
val clickActionLabel = stringResource(R.string.card_tap_action)
Card(
@ -90,7 +90,7 @@ fun NewsResourceCardExpanded(
// Pass null for action to only override the label and not the actual action.
modifier = modifier.semantics {
onClick(label = clickActionLabel, action = null)
}
},
) {
Column {
if (!userNewsResource.headerImageUrl.isNullOrEmpty()) {
@ -99,14 +99,14 @@ fun NewsResourceCardExpanded(
}
}
Box(
modifier = Modifier.padding(16.dp)
modifier = Modifier.padding(16.dp),
) {
Column {
Spacer(modifier = Modifier.height(12.dp))
Row {
NewsResourceTitle(
userNewsResource.title,
modifier = Modifier.fillMaxWidth((.8f))
modifier = Modifier.fillMaxWidth((.8f)),
)
Spacer(modifier = Modifier.weight(1f))
BookmarkButton(isBookmarked, onToggleBookmark)
@ -125,7 +125,7 @@ fun NewsResourceCardExpanded(
@Composable
fun NewsResourceHeaderImage(
headerImageUrl: String?
headerImageUrl: String?,
) {
AsyncImage(
placeholder = if (LocalInspectionMode.current) {
@ -140,14 +140,14 @@ fun NewsResourceHeaderImage(
contentScale = ContentScale.Crop,
model = headerImageUrl,
// TODO b/226661685: Investigate using alt text of image to populate content description
contentDescription = null // decorative image
contentDescription = null, // decorative image
)
}
@Composable
fun NewsResourceTitle(
newsResourceTitle: String,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
Text(newsResourceTitle, style = MaterialTheme.typography.headlineSmall, modifier = modifier)
}
@ -156,7 +156,7 @@ fun NewsResourceTitle(
fun BookmarkButton(
isBookmarked: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
NiaIconToggleButton(
checked = isBookmarked,
@ -165,15 +165,15 @@ fun BookmarkButton(
icon = {
Icon(
painter = painterResource(NiaIcons.BookmarkBorder),
contentDescription = stringResource(R.string.bookmark)
contentDescription = stringResource(R.string.bookmark),
)
},
checkedIcon = {
Icon(
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) {
val receiver = TimeZoneBroadcastReceiver(
onTimeZoneChanged = { zoneId = ZoneId.systemDefault() }
onTimeZoneChanged = { zoneId = ZoneId.systemDefault() },
)
receiver.register(context)
onDispose {
@ -200,7 +200,7 @@ fun dateFormatted(publishDate: Instant): String {
@Composable
fun NewsResourceMetaData(
publishDate: Instant,
resourceType: NewsResourceType
resourceType: NewsResourceType,
) {
val formattedDate = dateFormatted(publishDate)
Text(
@ -209,21 +209,21 @@ fun NewsResourceMetaData(
} else {
formattedDate
},
style = MaterialTheme.typography.labelSmall
style = MaterialTheme.typography.labelSmall,
)
}
@Composable
fun NewsResourceLink(
@Suppress("UNUSED_PARAMETER")
newsResource: NewsResource
newsResource: NewsResource,
) {
TODO()
}
@Composable
fun NewsResourceShortDescription(
newsResourceShortDescription: String
newsResourceShortDescription: String,
) {
Text(newsResourceShortDescription, style = MaterialTheme.typography.bodyLarge)
}
@ -231,7 +231,7 @@ fun NewsResourceShortDescription(
@Composable
fun NewsResourceTopics(
topics: List<FollowableTopic>,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
// 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.
@ -255,21 +255,21 @@ fun NewsResourceTopics(
val contentDescription = if (followableTopic.isFollowed) {
stringResource(
R.string.topic_chip_content_description_when_followed,
followableTopic.topic.name
followableTopic.topic.name,
)
} else {
stringResource(
R.string.topic_chip_content_description_when_not_followed,
followableTopic.topic.name
followableTopic.topic.name,
)
}
Text(
text = followableTopic.topic.name.uppercase(Locale.getDefault()),
modifier = Modifier.semantics {
this.contentDescription = contentDescription
}
},
)
}
},
)
}
}
@ -304,7 +304,7 @@ private fun ExpandedNewsResourcePreview() {
userNewsResource = previewUserNewsResources[0],
isBookmarked = true,
onToggleBookmark = {},
onClick = {}
onClick = {},
)
}
}

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

Loading…
Cancel
Save