Merge branch 'main' into dependabot/gradle/protobuf-4.28.2

pull/1630/head
Ben Weiss 10 months ago committed by GitHub
commit 2f0940d3c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,3 +5,13 @@
ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test ktlint_function_naming_ignore_when_annotated_with=Composable, Test
ktlint_standard_backing-property-naming = disabled
ktlint_standard_binary-expression-wrapping = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_condition-wrapping = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-type-modifier-spacing = disabled
ktlint_standard_multiline-loop = disabled
ktlint_standard_function-signature = disabled

@ -116,7 +116,7 @@ To run the tests execute the following gradle tasks:
- `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant. - `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant.
**Note:** You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute **Note:** You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute
tests against _all_ build variants which is both unecessary and will result in failures as only the tests against _all_ build variants which is both unnecessary and will result in failures as only the
`demoDebug` variant is supported. No other variants have any tests (although this might change in future). `demoDebug` variant is supported. No other variants have any tests (although this might change in future).
## Screenshot tests ## Screenshot tests

@ -1,56 +1,56 @@
androidx.activity:activity-compose:1.9.2 androidx.activity:activity-compose:1.9.3
androidx.activity:activity-ktx:1.9.2 androidx.activity:activity-ktx:1.9.3
androidx.activity:activity:1.9.2 androidx.activity:activity:1.9.3
androidx.annotation:annotation-experimental:1.4.0 androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.0 androidx.annotation:annotation-jvm:1.8.1
androidx.annotation:annotation:1.8.0 androidx.annotation:annotation:1.8.1
androidx.appcompat:appcompat-resources:1.6.1 androidx.appcompat:appcompat-resources:1.6.1
androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0 androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0 androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0 androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-jvm:1.4.4
androidx.collection:collection-ktx:1.4.0 androidx.collection:collection-ktx:1.4.4
androidx.collection:collection:1.4.0 androidx.collection:collection:1.4.4
androidx.compose.animation:animation-android:1.7.0 androidx.compose.animation:animation-android:1.7.5
androidx.compose.animation:animation-core-android:1.7.0 androidx.compose.animation:animation-core-android:1.7.5
androidx.compose.animation:animation-core:1.7.0 androidx.compose.animation:animation-core:1.7.5
androidx.compose.animation:animation:1.7.0 androidx.compose.animation:animation:1.7.5
androidx.compose.foundation:foundation-android:1.7.0 androidx.compose.foundation:foundation-android:1.7.5
androidx.compose.foundation:foundation-layout-android:1.7.0 androidx.compose.foundation:foundation-layout-android:1.7.5
androidx.compose.foundation:foundation-layout:1.7.0 androidx.compose.foundation:foundation-layout:1.7.5
androidx.compose.foundation:foundation:1.7.0 androidx.compose.foundation:foundation:1.7.5
androidx.compose.material3.adaptive:adaptive-android:1.0.0 androidx.compose.material3.adaptive:adaptive-android:1.0.0
androidx.compose.material3.adaptive:adaptive:1.0.0 androidx.compose.material3.adaptive:adaptive:1.0.0
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0 androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0 androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
androidx.compose.material3:material3-android:1.3.0 androidx.compose.material3:material3-android:1.3.1
androidx.compose.material3:material3:1.3.0 androidx.compose.material3:material3:1.3.1
androidx.compose.material:material-icons-core-android:1.7.0 androidx.compose.material:material-icons-core-android:1.7.5
androidx.compose.material:material-icons-core:1.7.0 androidx.compose.material:material-icons-core:1.7.5
androidx.compose.material:material-icons-extended-android:1.7.0 androidx.compose.material:material-icons-extended-android:1.7.5
androidx.compose.material:material-icons-extended:1.7.0 androidx.compose.material:material-icons-extended:1.7.5
androidx.compose.material:material-ripple-android:1.7.0 androidx.compose.material:material-ripple-android:1.7.5
androidx.compose.material:material-ripple:1.7.0 androidx.compose.material:material-ripple:1.7.5
androidx.compose.runtime:runtime-android:1.7.0 androidx.compose.runtime:runtime-android:1.7.5
androidx.compose.runtime:runtime-saveable-android:1.7.0 androidx.compose.runtime:runtime-saveable-android:1.7.5
androidx.compose.runtime:runtime-saveable:1.7.0 androidx.compose.runtime:runtime-saveable:1.7.5
androidx.compose.runtime:runtime:1.7.0 androidx.compose.runtime:runtime:1.7.5
androidx.compose.ui:ui-android:1.7.0 androidx.compose.ui:ui-android:1.7.5
androidx.compose.ui:ui-geometry-android:1.7.0 androidx.compose.ui:ui-geometry-android:1.7.5
androidx.compose.ui:ui-geometry:1.7.0 androidx.compose.ui:ui-geometry:1.7.5
androidx.compose.ui:ui-graphics-android:1.7.0 androidx.compose.ui:ui-graphics-android:1.7.5
androidx.compose.ui:ui-graphics:1.7.0 androidx.compose.ui:ui-graphics:1.7.5
androidx.compose.ui:ui-text-android:1.7.0 androidx.compose.ui:ui-text-android:1.7.5
androidx.compose.ui:ui-text:1.7.0 androidx.compose.ui:ui-text:1.7.5
androidx.compose.ui:ui-tooling-preview-android:1.7.0 androidx.compose.ui:ui-tooling-preview-android:1.7.5
androidx.compose.ui:ui-tooling-preview:1.7.0 androidx.compose.ui:ui-tooling-preview:1.7.5
androidx.compose.ui:ui-unit-android:1.7.0 androidx.compose.ui:ui-unit-android:1.7.5
androidx.compose.ui:ui-unit:1.7.0 androidx.compose.ui:ui-unit:1.7.5
androidx.compose.ui:ui-util-android:1.7.0 androidx.compose.ui:ui-util-android:1.7.5
androidx.compose.ui:ui-util:1.7.0 androidx.compose.ui:ui-util:1.7.5
androidx.compose.ui:ui:1.7.0 androidx.compose.ui:ui:1.7.5
androidx.compose:compose-bom:2024.09.00 androidx.compose:compose-bom:2024.11.00
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1 androidx.core:core-ktx:1.13.1
androidx.core:core:1.13.1 androidx.core:core:1.13.1

@ -114,6 +114,7 @@ dependencies {
kspTest(libs.hilt.compiler) kspTest(libs.hilt.compiler)
testImplementation(projects.core.dataTest) testImplementation(projects.core.dataTest)
testImplementation(projects.core.datastoreTest)
testImplementation(libs.hilt.android.testing) testImplementation(libs.hilt.android.testing)
testImplementation(projects.sync.syncTest) testImplementation(projects.sync.syncTest)
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)
@ -122,7 +123,6 @@ dependencies {
testDemoImplementation(libs.roborazzi) testDemoImplementation(libs.roborazzi)
testDemoImplementation(projects.core.screenshotTesting) testDemoImplementation(projects.core.screenshotTesting)
androidTestImplementation(kotlin("test"))
androidTestImplementation(projects.core.testing) androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.dataTest) androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest) androidTestImplementation(projects.core.datastoreTest)
@ -130,6 +130,7 @@ dependencies {
androidTestImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlin.test)
baselineProfile(projects.benchmarks) baselineProfile(projects.benchmarks)
} }

@ -1,6 +1,6 @@
androidx.activity:activity-compose:1.9.2 androidx.activity:activity-compose:1.9.3
androidx.activity:activity-ktx:1.9.2 androidx.activity:activity-ktx:1.9.3
androidx.activity:activity:1.9.2 androidx.activity:activity:1.9.3
androidx.annotation:annotation-experimental:1.4.1 androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.1 androidx.annotation:annotation-jvm:1.8.1
androidx.annotation:annotation:1.8.1 androidx.annotation:annotation:1.8.1
@ -10,55 +10,55 @@ androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0 androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0 androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0 androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.4.2 androidx.collection:collection-jvm:1.4.4
androidx.collection:collection-ktx:1.4.2 androidx.collection:collection-ktx:1.4.4
androidx.collection:collection:1.4.2 androidx.collection:collection:1.4.4
androidx.compose.animation:animation-android:1.7.0 androidx.compose.animation:animation-android:1.7.5
androidx.compose.animation:animation-core-android:1.7.0 androidx.compose.animation:animation-core-android:1.7.5
androidx.compose.animation:animation-core:1.7.0 androidx.compose.animation:animation-core:1.7.5
androidx.compose.animation:animation:1.7.0 androidx.compose.animation:animation:1.7.5
androidx.compose.foundation:foundation-android:1.7.0 androidx.compose.foundation:foundation-android:1.7.5
androidx.compose.foundation:foundation-layout-android:1.7.0 androidx.compose.foundation:foundation-layout-android:1.7.5
androidx.compose.foundation:foundation-layout:1.7.0 androidx.compose.foundation:foundation-layout:1.7.5
androidx.compose.foundation:foundation:1.7.0 androidx.compose.foundation:foundation:1.7.5
androidx.compose.material3.adaptive:adaptive-android:1.0.0 androidx.compose.material3.adaptive:adaptive-android:1.0.0
androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0 androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0
androidx.compose.material3.adaptive:adaptive-layout:1.0.0 androidx.compose.material3.adaptive:adaptive-layout:1.0.0
androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0 androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0
androidx.compose.material3.adaptive:adaptive-navigation:1.0.0 androidx.compose.material3.adaptive:adaptive-navigation:1.0.0
androidx.compose.material3.adaptive:adaptive:1.0.0 androidx.compose.material3.adaptive:adaptive:1.0.0
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0 androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0 androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
androidx.compose.material3:material3-android:1.3.0 androidx.compose.material3:material3-android:1.3.1
androidx.compose.material3:material3-window-size-class-android:1.3.0 androidx.compose.material3:material3-window-size-class-android:1.3.1
androidx.compose.material3:material3-window-size-class:1.3.0 androidx.compose.material3:material3-window-size-class:1.3.1
androidx.compose.material3:material3:1.3.0 androidx.compose.material3:material3:1.3.1
androidx.compose.material:material-icons-core-android:1.7.0 androidx.compose.material:material-icons-core-android:1.7.5
androidx.compose.material:material-icons-core:1.7.0 androidx.compose.material:material-icons-core:1.7.5
androidx.compose.material:material-icons-extended-android:1.7.0 androidx.compose.material:material-icons-extended-android:1.7.5
androidx.compose.material:material-icons-extended:1.7.0 androidx.compose.material:material-icons-extended:1.7.5
androidx.compose.material:material-ripple-android:1.7.0 androidx.compose.material:material-ripple-android:1.7.5
androidx.compose.material:material-ripple:1.7.0 androidx.compose.material:material-ripple:1.7.5
androidx.compose.runtime:runtime-android:1.7.1 androidx.compose.runtime:runtime-android:1.7.5
androidx.compose.runtime:runtime-saveable-android:1.7.1 androidx.compose.runtime:runtime-saveable-android:1.7.5
androidx.compose.runtime:runtime-saveable:1.7.1 androidx.compose.runtime:runtime-saveable:1.7.5
androidx.compose.runtime:runtime-tracing:1.0.0-beta01 androidx.compose.runtime:runtime-tracing:1.7.5
androidx.compose.runtime:runtime:1.7.1 androidx.compose.runtime:runtime:1.7.5
androidx.compose.ui:ui-android:1.7.0 androidx.compose.ui:ui-android:1.7.5
androidx.compose.ui:ui-geometry-android:1.7.0 androidx.compose.ui:ui-geometry-android:1.7.5
androidx.compose.ui:ui-geometry:1.7.0 androidx.compose.ui:ui-geometry:1.7.5
androidx.compose.ui:ui-graphics-android:1.7.0 androidx.compose.ui:ui-graphics-android:1.7.5
androidx.compose.ui:ui-graphics:1.7.0 androidx.compose.ui:ui-graphics:1.7.5
androidx.compose.ui:ui-text-android:1.7.0 androidx.compose.ui:ui-text-android:1.7.5
androidx.compose.ui:ui-text:1.7.0 androidx.compose.ui:ui-text:1.7.5
androidx.compose.ui:ui-tooling-preview-android:1.7.0 androidx.compose.ui:ui-tooling-preview-android:1.7.5
androidx.compose.ui:ui-tooling-preview:1.7.0 androidx.compose.ui:ui-tooling-preview:1.7.5
androidx.compose.ui:ui-unit-android:1.7.0 androidx.compose.ui:ui-unit-android:1.7.5
androidx.compose.ui:ui-unit:1.7.0 androidx.compose.ui:ui-unit:1.7.5
androidx.compose.ui:ui-util-android:1.7.0 androidx.compose.ui:ui-util-android:1.7.5
androidx.compose.ui:ui-util:1.7.0 androidx.compose.ui:ui-util:1.7.5
androidx.compose.ui:ui:1.7.0 androidx.compose.ui:ui:1.7.5
androidx.compose:compose-bom:2024.09.00 androidx.compose:compose-bom:2024.11.00
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1 androidx.core:core-ktx:1.13.1
androidx.core:core-splashscreen:1.0.1 androidx.core:core-splashscreen:1.0.1

@ -105,9 +105,9 @@ application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml'
application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml' application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon='' launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon=''
uses-library-not-required:'android.ext.adservices'
uses-library-not-required:'androidx.window.extensions' uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar' uses-library-not-required:'androidx.window.sidecar'
uses-library-not-required:'android.ext.adservices'
feature-group: label='' feature-group: label=''
uses-feature: name='android.hardware.faketouch' uses-feature: name='android.hardware.faketouch'
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'

@ -16,13 +16,16 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
@ -32,10 +35,10 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -43,7 +46,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import javax.inject.Inject import javax.inject.Inject
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR 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.foryou.R as FeatureForyouR
@ -62,29 +64,24 @@ class NavigationTest {
@get:Rule(order = 0) @get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this) val hiltRule = HiltAndroidRule(this)
/**
* 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)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/** /**
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission. * Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
*/ */
@get:Rule(order = 2) @get:Rule(order = 1)
val postNotificationsPermission = GrantPostNotificationsPermissionRule() val postNotificationsPermission = GrantPostNotificationsPermissionRule()
/** /**
* Use the primary activity to initialize the app normally. * Use the primary activity to initialize the app normally.
*/ */
@get:Rule(order = 3) @get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<MainActivity>() val composeTestRule = createAndroidComposeRule<MainActivity>()
@Inject @Inject
lateinit var topicsRepository: TopicsRepository lateinit var topicsRepository: TopicsRepository
@Inject
lateinit var newsRepository: NewsRepository
// The strings used for matching in these tests // The strings used for matching in these tests
private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up) private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up)
private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title) private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title)
@ -277,4 +274,44 @@ class NavigationTest {
onNodeWithTag("topic:${topic.id}").assertExists() onNodeWithTag("topic:${topic.id}").assertExists()
} }
} }
@Test
fun navigatingToTopicFromForYou_showsTopicDetails() {
composeTestRule.apply {
// Get the first news resource
val newsResource = runBlocking {
newsRepository.getNewsResources().first().first()
}
// Get its first topic and follow it
val topic = newsResource.topics.first()
onNodeWithText(topic.name).performClick()
// Get the news feed and scroll to the news resource
// Note: Possible flakiness. If the content of the news resource is long then the topic
// tag might not be visible meaning it cannot be clicked
onNodeWithTag("forYou:feed")
.performScrollToNode(hasTestTag("newsResourceCard:${newsResource.id}"))
.fetchSemanticsNode()
.apply {
val newsResourceCardNode = onNodeWithTag("newsResourceCard:${newsResource.id}")
.fetchSemanticsNode()
config[ScrollBy].action?.invoke(
0f,
// to ensure the bottom of the card is visible,
// manually scroll the difference between the height of
// the scrolling node and the height of the card
(newsResourceCardNode.size.height - size.height).coerceAtLeast(0).toFloat(),
)
}
// Click the first topic tag
onAllNodesWithTag("topicTag:${topic.id}", useUnmergedTree = true)
.onFirst()
.performClick()
// Verify that we're on the correct topic details screen
onNodeWithTag("topic:${topic.id}").assertExists()
}
}
} }

@ -20,10 +20,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouSection
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests
import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import com.google.samples.apps.nowinandroid.ui.NiaAppState import com.google.samples.apps.nowinandroid.ui.NiaAppState
import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen
@ -44,10 +46,18 @@ fun NiaNavHost(
val navController = appState.navController val navController = appState.navController
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = ForYouRoute, startDestination = ForYouBaseRoute,
modifier = modifier, modifier = modifier,
) { ) {
forYouScreen(onTopicClick = navController::navigateToInterests) forYouSection(
onTopicClick = navController::navigateToTopic,
) {
topicScreen(
showBackButton = true,
onBackClick = navController::popBackStack,
onTopicClick = navController::navigateToTopic,
)
}
bookmarksScreen( bookmarksScreen(
onTopicClick = navController::navigateToInterests, onTopicClick = navController::navigateToInterests,
onShowSnackbar = onShowSnackbar, onShowSnackbar = onShowSnackbar,

@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -29,9 +30,18 @@ import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
import com.google.samples.apps.nowinandroid.feature.search.R as searchR import com.google.samples.apps.nowinandroid.feature.search.R as searchR
/** /**
* Type for the top level destinations in the application. Each of these destinations * Type for the top level destinations in the application. Contains metadata about the destination
* can contain one or more screens (based on the window size). Navigation from one screen to the * that is used in the top app bar and common navigation UI.
* next within a single destination will be handled directly in composables. *
* @param selectedIcon The icon to be displayed in the navigation UI when this destination is
* selected.
* @param unselectedIcon The icon to be displayed in the navigation UI when this destination is
* not selected.
* @param iconTextId Text that to be displayed in the navigation UI.
* @param titleTextId Text that is displayed on the top app bar.
* @param route The route to use when navigating to this destination.
* @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that
* there is a single destination in that section of the app (no nested destinations).
*/ */
enum class TopLevelDestination( enum class TopLevelDestination(
val selectedIcon: ImageVector, val selectedIcon: ImageVector,
@ -39,6 +49,7 @@ enum class TopLevelDestination(
@StringRes val iconTextId: Int, @StringRes val iconTextId: Int,
@StringRes val titleTextId: Int, @StringRes val titleTextId: Int,
val route: KClass<*>, val route: KClass<*>,
val baseRoute: KClass<*> = route,
) { ) {
FOR_YOU( FOR_YOU(
selectedIcon = NiaIcons.Upcoming, selectedIcon = NiaIcons.Upcoming,
@ -46,6 +57,7 @@ enum class TopLevelDestination(
iconTextId = forYouR.string.feature_foryou_title, iconTextId = forYouR.string.feature_foryou_title,
titleTextId = R.string.app_name, titleTextId = R.string.app_name,
route = ForYouRoute::class, route = ForYouRoute::class,
baseRoute = ForYouBaseRoute::class,
), ),
BOOKMARKS( BOOKMARKS(
selectedIcon = NiaIcons.Bookmarks, selectedIcon = NiaIcons.Bookmarks,

@ -152,7 +152,7 @@ internal fun NiaApp(
appState.topLevelDestinations.forEach { destination -> appState.topLevelDestinations.forEach { destination ->
val hasUnread = unreadDestinations.contains(destination) val hasUnread = unreadDestinations.contains(destination)
val selected = currentDestination val selected = currentDestination
.isRouteInHierarchy(destination.route) .isRouteInHierarchy(destination.baseRoute)
item( item(
selected = selected, selected = selected,
onClick = { appState.navigateToTopLevelDestination(destination) }, onClick = { appState.navigateToTopLevelDestination(destination) },

@ -90,7 +90,7 @@ class NiaAppState(
val currentTopLevelDestination: TopLevelDestination? val currentTopLevelDestination: TopLevelDestination?
@Composable get() { @Composable get() {
return TopLevelDestination.entries.firstOrNull { topLevelDestination -> return TopLevelDestination.entries.firstOrNull { topLevelDestination ->
currentDestination?.hasRoute(route = topLevelDestination.route) ?: false currentDestination?.hasRoute(route = topLevelDestination.route) == true
} }
} }

@ -15,9 +15,6 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<!-- Status bar -->
<color name="black30">#4D000000</color>
<color name="ic_launcher_background_tint">#000000</color> <color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FCFCFC</color> <color name="ic_launcher_foreground_tint">#FCFCFC</color>
</resources> </resources>

@ -31,7 +31,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.ui.interests2pane.InterestsListDetailScreen import com.google.samples.apps.nowinandroid.ui.interests2pane.InterestsListDetailScreen
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
@ -40,7 +39,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ -60,11 +58,7 @@ class InterestsListDetailScreenTest {
@get:Rule(order = 0) @get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this) val hiltRule = HiltAndroidRule(this)
@BindValue
@get:Rule(order = 1) @get:Rule(order = 1)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>() val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject @Inject

@ -38,7 +38,6 @@ import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
@ -47,7 +46,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ -74,18 +72,10 @@ class NiaAppScreenSizesScreenshotTests {
@get:Rule(order = 0) @get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this) val hiltRule = HiltAndroidRule(this)
/**
* 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)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/** /**
* Use a test activity to set the content on. * Use a test activity to set the content on.
*/ */
@get:Rule(order = 2) @get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>() val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject @Inject

@ -69,7 +69,6 @@ import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
@ -80,7 +79,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ -107,18 +105,10 @@ class SnackbarInsetsScreenshotTests {
@get:Rule(order = 0) @get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this) val hiltRule = HiltAndroidRule(this)
/**
* 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)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/** /**
* Use a test activity to set the content on. * Use a test activity to set the content on.
*/ */
@get:Rule(order = 2) @get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>() val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject @Inject

@ -42,7 +42,6 @@ import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
@ -53,7 +52,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ -80,18 +78,10 @@ class SnackbarScreenshotTests {
@get:Rule(order = 0) @get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this) val hiltRule = HiltAndroidRule(this)
/**
* 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)
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
/** /**
* Use a test activity to set the content on. * Use a test activity to set the content on.
*/ */
@get:Rule(order = 2) @get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>() val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Inject @Inject

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 94 KiB

@ -53,8 +53,8 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
disableUnnecessaryAndroidTests(target) disableUnnecessaryAndroidTests(target)
} }
dependencies { dependencies {
add("androidTestImplementation", kotlin("test")) add("androidTestImplementation", libs.findLibrary("kotlin.test").get())
add("testImplementation", kotlin("test")) add("testImplementation", libs.findLibrary("kotlin.test").get())
add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) add("implementation", libs.findLibrary("androidx.tracing.ktx").get())
} }

@ -26,7 +26,13 @@ class HiltConventionPlugin : Plugin<Project> {
pluginManager.apply("com.google.devtools.ksp") pluginManager.apply("com.google.devtools.ksp")
dependencies { dependencies {
add("ksp", libs.findLibrary("hilt.compiler").get()) add("ksp", libs.findLibrary("hilt.compiler").get())
add("implementation", libs.findLibrary("hilt.core").get()) }
// Add support for Jvm Module, base on org.jetbrains.kotlin.jvm
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
dependencies {
add("implementation", libs.findLibrary("hilt.core").get())
}
} }
/** Add support for Android modules, based on [AndroidBasePlugin] */ /** Add support for Android modules, based on [AndroidBasePlugin] */

@ -15,8 +15,11 @@
*/ */
import com.google.samples.apps.nowinandroid.configureKotlinJvm import com.google.samples.apps.nowinandroid.configureKotlinJvm
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.kotlin
class JvmLibraryConventionPlugin : Plugin<Project> { class JvmLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
@ -26,6 +29,9 @@ class JvmLibraryConventionPlugin : Plugin<Project> {
apply("nowinandroid.android.lint") apply("nowinandroid.android.lint")
} }
configureKotlinJvm() configureKotlinJvm()
dependencies {
add("testImplementation", libs.findLibrary("kotlin.test").get())
}
} }
} }
} }

@ -30,6 +30,6 @@ import org.gradle.api.Project
internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests( internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
project: Project, project: Project,
) = beforeVariants { ) = beforeVariants {
it.enableAndroidTest = it.enableAndroidTest it.androidTest.enable = it.androidTest.enable
&& project.projectDir.resolve("src/androidTest").exists() && project.projectDir.resolve("src/androidTest").exists()
} }

@ -16,7 +16,13 @@
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { repositories {
google() google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
} }
versionCatalogs { versionCatalogs {

@ -16,7 +16,13 @@
buildscript { buildscript {
repositories { repositories {
google() google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
// Android Build Server // Android Build Server
@ -30,7 +36,13 @@ buildscript {
} }
// Lists all plugins used throughout the project /*
* By listing all the plugins used throughout all subprojects in the root project build script, it
* ensures that the build script classpath remains the same for all projects. This avoids potential
* problems with mismatching versions of transitive plugin dependencies. A subproject that applies
* an unlisted plugin will have that plugin and its dependencies _appended_ to the classpath, not
* replacing pre-existing dependencies.
*/
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false

@ -1,5 +1,5 @@
// This file contains classes (with possible wildcards) that the Compose Compiler will treat as stable. // This file contains classes (with possible wildcards) that the Compose Compiler will treat as stable.
// It allows us to define classes that our not part of our codebase without wrapping them in a stable class. // It allows us to define classes that are not part of our codebase without wrapping them in a stable class.
// For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file // For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file
// We always use immutable classes for our data model, to avoid running the Compose compiler // We always use immutable classes for our data model, to avoid running the Compose compiler

@ -17,16 +17,13 @@
package com.google.samples.apps.nowinandroid.core.data.test.repository package com.google.samples.apps.nowinandroid.core.data.test.repository
import com.google.samples.apps.nowinandroid.core.data.Synchronizer import com.google.samples.apps.nowinandroid.core.data.Synchronizer
import com.google.samples.apps.nowinandroid.core.data.model.asEntity import com.google.samples.apps.nowinandroid.core.data.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@ -48,9 +45,11 @@ class FakeNewsRepository @Inject constructor(
query: NewsResourceQuery, query: NewsResourceQuery,
): Flow<List<NewsResource>> = ): Flow<List<NewsResource>> =
flow { flow {
val newsResources = datasource.getNewsResources()
val topics = datasource.getTopics()
emit( emit(
datasource newsResources
.getNewsResources()
.filter { networkNewsResource -> .filter { networkNewsResource ->
// Filter out any news resources which don't match the current query. // Filter out any news resources which don't match the current query.
// If no query parameters (filterTopicIds or filterNewsIds) are specified // If no query parameters (filterTopicIds or filterNewsIds) are specified
@ -64,8 +63,7 @@ class FakeNewsRepository @Inject constructor(
) )
.all(true::equals) .all(true::equals)
} }
.map(NetworkNewsResource::asEntity) .map { it.asExternalModel(topics) },
.map(NewsResourceEntity::asExternalModel),
) )
}.flowOn(ioDispatcher) }.flowOn(ioDispatcher)

@ -19,8 +19,10 @@ package com.google.samples.apps.nowinandroid.core.data.model
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel
fun NetworkNewsResource.asEntity() = NewsResourceEntity( fun NetworkNewsResource.asEntity() = NewsResourceEntity(
id = id, id = id,
@ -32,16 +34,6 @@ fun NetworkNewsResource.asEntity() = NewsResourceEntity(
type = type, type = type,
) )
fun NetworkNewsResourceExpanded.asEntity() = NewsResourceEntity(
id = id,
title = title,
content = content,
url = url,
headerImageUrl = headerImageUrl,
publishDate = publishDate,
type = type,
)
/** /**
* A shell [TopicEntity] to fulfill the foreign key constraint when inserting * A shell [TopicEntity] to fulfill the foreign key constraint when inserting
* a [NewsResourceEntity] into the DB * a [NewsResourceEntity] into the DB
@ -65,3 +57,17 @@ fun NetworkNewsResource.topicCrossReferences(): List<NewsResourceTopicCrossRef>
topicId = topicId, topicId = topicId,
) )
} }
fun NetworkNewsResource.asExternalModel(topics: List<NetworkTopic>) =
NewsResource(
id = id,
title = title,
content = content,
url = url,
headerImageUrl = headerImageUrl,
publishDate = publishDate,
type = type,
topics = topics
.filter { networkTopic -> this.topics.contains(networkTopic.id) }
.map(NetworkTopic::asExternalModel),
)

@ -1,91 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.data.model
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 kotlinx.datetime.Instant
import org.junit.Test
import kotlin.test.assertEquals
class NetworkEntityKtTest {
@Test
fun network_topic_can_be_mapped_to_topic_entity() {
val networkModel = NetworkTopic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("Test", entity.name)
assertEquals("short description", entity.shortDescription)
assertEquals("long description", entity.longDescription)
assertEquals("URL", entity.url)
assertEquals("image URL", entity.imageUrl)
}
@Test
fun network_news_resource_can_be_mapped_to_news_resource_entity() {
val networkModel =
NetworkNewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("title", entity.title)
assertEquals("content", entity.content)
assertEquals("url", entity.url)
assertEquals("headerImageUrl", entity.headerImageUrl)
assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
assertEquals("Article 📚", entity.type)
val expandedNetworkModel =
NetworkNewsResourceExpanded(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
)
val entityFromExpanded = expandedNetworkModel.asEntity()
assertEquals("0", entityFromExpanded.id)
assertEquals("title", entityFromExpanded.title)
assertEquals("content", entityFromExpanded.content)
assertEquals("url", entityFromExpanded.url)
assertEquals("headerImageUrl", entityFromExpanded.headerImageUrl)
assertEquals(Instant.fromEpochMilliseconds(1), entityFromExpanded.publishDate)
assertEquals("Article 📚", entityFromExpanded.type)
}
}

@ -0,0 +1,140 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.data.model
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel
import kotlinx.datetime.Instant
import org.junit.Test
import kotlin.test.assertEquals
class NetworkEntityTest {
@Test
fun networkTopicMapsToDatabaseModel() {
val networkModel = NetworkTopic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "image URL",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("Test", entity.name)
assertEquals("short description", entity.shortDescription)
assertEquals("long description", entity.longDescription)
assertEquals("URL", entity.url)
assertEquals("image URL", entity.imageUrl)
}
@Test
fun networkNewsResourceMapsToDatabaseModel() {
val networkModel =
NetworkNewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
)
val entity = networkModel.asEntity()
assertEquals("0", entity.id)
assertEquals("title", entity.title)
assertEquals("content", entity.content)
assertEquals("url", entity.url)
assertEquals("headerImageUrl", entity.headerImageUrl)
assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
assertEquals("Article 📚", entity.type)
}
@Test
fun networkTopicMapsToExternalModel() {
val networkTopic = NetworkTopic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "imageUrl",
)
val expected = Topic(
id = "0",
name = "Test",
shortDescription = "short description",
longDescription = "long description",
url = "URL",
imageUrl = "imageUrl",
)
assertEquals(expected, networkTopic.asExternalModel())
}
@Test
fun networkNewsResourceMapsToExternalModel() {
val networkNewsResource = NetworkNewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
topics = listOf("1", "2"),
)
val networkTopics = listOf(
NetworkTopic(
id = "1",
name = "Test 1",
shortDescription = "short description 1",
longDescription = "long description 1",
url = "url 1",
imageUrl = "imageUrl 1",
),
NetworkTopic(
id = "2",
name = "Test 2",
shortDescription = "short description 2",
longDescription = "long description 2",
url = "url 2",
imageUrl = "imageUrl 2",
),
)
val expected = NewsResource(
id = "0",
title = "title",
content = "content",
url = "url",
headerImageUrl = "headerImageUrl",
publishDate = Instant.fromEpochMilliseconds(1),
type = "Article 📚",
topics = networkTopics.map(NetworkTopic::asExternalModel),
)
assertEquals(expected, networkNewsResource.asExternalModel(networkTopics))
}
}

@ -32,7 +32,8 @@ import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsRes
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
@ -43,9 +44,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -67,14 +66,9 @@ class OfflineFirstNewsRepositoryTest {
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before @Before
fun setup() { fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource( niaPreferencesDataSource = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
tmpFolder.testUserPreferencesDataStore(testScope),
)
newsResourceDao = TestNewsResourceDao() newsResourceDao = TestNewsResourceDao()
topicDao = TestTopicDao() topicDao = TestTopicDao()
network = TestNiaNetworkDataSource() network = TestNiaNetworkDataSource()

@ -25,7 +25,8 @@ import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -33,9 +34,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals import kotlin.test.assertEquals
class OfflineFirstTopicsRepositoryTest { class OfflineFirstTopicsRepositoryTest {
@ -52,16 +51,11 @@ class OfflineFirstTopicsRepositoryTest {
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before @Before
fun setup() { fun setup() {
topicDao = TestTopicDao() topicDao = TestTopicDao()
network = TestNiaNetworkDataSource() network = TestNiaNetworkDataSource()
niaPreferences = NiaPreferencesDataSource( niaPreferences = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
tmpFolder.testUserPreferencesDataStore(testScope),
)
synchronizer = TestSynchronizer(niaPreferences) synchronizer = TestSynchronizer(niaPreferences)
subject = OfflineFirstTopicsRepository( subject = OfflineFirstTopicsRepository(

@ -18,7 +18,8 @@ package com.google.samples.apps.nowinandroid.core.data.repository
import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
@ -28,9 +29,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -45,14 +44,9 @@ class OfflineFirstUserDataRepositoryTest {
private val analyticsHelper = NoOpAnalyticsHelper() private val analyticsHelper = NoOpAnalyticsHelper()
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before @Before
fun setup() { fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource( niaPreferencesDataSource = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
tmpFolder.testUserPreferencesDataStore(testScope),
)
subject = OfflineFirstUserDataRepository( subject = OfflineFirstUserDataRepository(
niaPreferencesDataSource = niaPreferencesDataSource, niaPreferencesDataSource = niaPreferencesDataSource,

@ -0,0 +1,28 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.datastore.test
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.updateAndGet
class InMemoryDataStore<T>(initialValue: T) : DataStore<T> {
override val data = MutableStateFlow(initialValue)
override suspend fun updateData(
transform: suspend (it: T) -> T,
) = data.updateAndGet { transform(it) }
}

@ -17,17 +17,13 @@
package com.google.samples.apps.nowinandroid.core.datastore.test package com.google.samples.apps.nowinandroid.core.datastore.test
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer
import com.google.samples.apps.nowinandroid.core.datastore.di.DataStoreModule import com.google.samples.apps.nowinandroid.core.datastore.di.DataStoreModule
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn import dagger.hilt.testing.TestInstallIn
import kotlinx.coroutines.CoroutineScope
import org.junit.rules.TemporaryFolder
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -36,26 +32,9 @@ import javax.inject.Singleton
replaces = [DataStoreModule::class], replaces = [DataStoreModule::class],
) )
internal object TestDataStoreModule { internal object TestDataStoreModule {
@Provides @Provides
@Singleton @Singleton
fun providesUserPreferencesDataStore( fun providesUserPreferencesDataStore(
@ApplicationScope scope: CoroutineScope, serializer: UserPreferencesSerializer,
userPreferencesSerializer: UserPreferencesSerializer, ): DataStore<UserPreferences> = InMemoryDataStore(serializer.defaultValue)
tmpFolder: TemporaryFolder,
): DataStore<UserPreferences> =
tmpFolder.testUserPreferencesDataStore(
coroutineScope = scope,
userPreferencesSerializer = userPreferencesSerializer,
)
}
fun TemporaryFolder.testUserPreferencesDataStore(
coroutineScope: CoroutineScope,
userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer(),
) = DataStoreFactory.create(
serializer = userPreferencesSerializer,
scope = coroutineScope,
) {
newFile("user_preferences_test.pb")
} }

@ -16,15 +16,13 @@
package com.google.samples.apps.nowinandroid.core.datastore package com.google.samples.apps.nowinandroid.core.datastore
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore import com.google.samples.apps.nowinandroid.core.datastore.test.InMemoryDataStore
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -34,14 +32,9 @@ class NiaPreferencesDataSourceTest {
private lateinit var subject: NiaPreferencesDataSource private lateinit var subject: NiaPreferencesDataSource
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before @Before
fun setup() { fun setup() {
subject = NiaPreferencesDataSource( subject = NiaPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
tmpFolder.testUserPreferencesDataStore(testScope),
)
} }
@Test @Test

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 575 B

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

Loading…
Cancel
Save