Merge branch 'main' into andres/add-gmd

pull/1208/head
Andres Sandoval 2 years ago committed by GitHub
commit 971232e7b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v2
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
@ -37,7 +37,7 @@ jobs:
java-version: 17 java-version: 17
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
- name: Check build-logic - name: Check build-logic
run: ./gradlew check -p build-logic run: ./gradlew check -p build-logic
@ -109,13 +109,9 @@ jobs:
- name: Build all build type and flavor permutations - name: Build all build type and flavor permutations
run: ./gradlew :app:assemble :benchmarks:assemble run: ./gradlew :app:assemble :benchmarks:assemble
-x pixel6Api33ProdNonMinifiedReleaseAndroidTest -x pixel6Api33ProdNonMinifiedReleaseAndroidTest
-x pixel6Api33ProdNonMinifiedBenchmarkAndroidTest
-x pixel6Api33DemoNonMinifiedReleaseAndroidTest -x pixel6Api33DemoNonMinifiedReleaseAndroidTest
-x pixel6Api33DemoNonMinifiedBenchmarkAndroidTest
-x collectDemoNonMinifiedReleaseBaselineProfile -x collectDemoNonMinifiedReleaseBaselineProfile
-x collectDemoNonMinifiedBenchmarkBaselineProfile
-x collectProdNonMinifiedReleaseBaselineProfile -x collectProdNonMinifiedReleaseBaselineProfile
-x collectProdNonMinifiedBenchmarkBaselineProfile
- name: Upload build outputs (APKs) - name: Upload build outputs (APKs)
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -143,7 +139,6 @@ jobs:
- name: Check badging - name: Check badging
run: ./gradlew :app:checkProdReleaseBadging run: ./gradlew :app:checkProdReleaseBadging
# androidTest: # androidTest:
# runs-on: macOS-latest # enables hardware acceleration in the virtual machine # runs-on: macOS-latest # enables hardware acceleration in the virtual machine
# timeout-minutes: 55 # timeout-minutes: 55
@ -191,6 +186,25 @@ jobs:
runs-on: macOS-latest # enables hardware acceleration in the virtual machine runs-on: macOS-latest # enables hardware acceleration in the virtual machine
timeout-minutes: 90 timeout-minutes: 90
steps:
- name: Delete unnecessary tools 🔧
uses: jlumbroso/free-disk-space@v1.3.1
with:
android: false # Don't remove Android tools
tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY"
dotnet: true # rm -rf /usr/share/dotnet
haskell: true # rm -rf /opt/ghc...
swap-storage: true # rm -f /mnt/swapfile (4GiB)
docker-images: false # Takes 16s, enable if needed in the future
large-packages: false # includes google-cloud-sdk and it's slow
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls /dev/kvm
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -205,7 +219,7 @@ jobs:
java-version: 17 java-version: 17
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
- name: Accept Android licenses - name: Accept Android licenses
run: yes | "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses || true run: yes | "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses || true

@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v2
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties

@ -122,9 +122,9 @@ tests against _all_ build variants which is both unecessary and will result in f
A screenshot test takes a screenshot of a screen or a UI component within the app, and compares it A screenshot test takes a screenshot of a screen or a UI component within the app, and compares it
with a previously recorded screenshot which is known to be rendered correctly. with a previously recorded screenshot which is known to be rendered correctly.
For example, Now in Android has [screenshot tests](https://github.com/android/nowinandroid/blob/main/app/src/testDemoDebug/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt) For example, Now in Android has [screenshot tests](https://github.com/android/nowinandroid/blob/main/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt)
to verify that the navigation is displayed correctly on different screen sizes to verify that the navigation is displayed correctly on different screen sizes
([known correct screenshots](https://github.com/android/nowinandroid/tree/main/app/src/testDemoDebug/screenshots)). ([known correct screenshots](https://github.com/android/nowinandroid/tree/main/app/src/testDemo/screenshots)).
Now In Android uses [Roborazzi](https://github.com/takahirom/roborazzi) to run screenshot tests Now In Android uses [Roborazzi](https://github.com/takahirom/roborazzi) to run screenshot tests
of certain screens and UI components. When working with screenshot tests the following gradle tasks are useful: of certain screens and UI components. When working with screenshot tests the following gradle tasks are useful:

@ -1,7 +1,7 @@
androidx.activity:activity-compose:1.8.0 androidx.activity:activity-compose:1.8.0
androidx.activity:activity-ktx:1.8.0 androidx.activity:activity-ktx:1.8.0
androidx.activity:activity:1.8.0 androidx.activity:activity:1.8.0
androidx.annotation:annotation-experimental:1.3.1 androidx.annotation:annotation-experimental:1.4.0
androidx.annotation:annotation-jvm:1.7.0 androidx.annotation:annotation-jvm:1.7.0
androidx.annotation:annotation:1.7.0 androidx.annotation:annotation:1.7.0
androidx.appcompat:appcompat-resources:1.6.1 androidx.appcompat:appcompat-resources:1.6.1
@ -9,48 +9,50 @@ 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.6.0 androidx.browser:browser:1.6.0
androidx.collection:collection-jvm:1.3.0 androidx.collection:collection-jvm:1.4.0
androidx.collection:collection:1.3.0 androidx.collection:collection-ktx:1.4.0
androidx.compose.animation:animation-android:1.5.4 androidx.collection:collection:1.4.0
androidx.compose.animation:animation-core-android:1.5.4 androidx.compose.animation:animation-android:1.6.1
androidx.compose.animation:animation-core:1.5.4 androidx.compose.animation:animation-core-android:1.6.1
androidx.compose.animation:animation:1.5.4 androidx.compose.animation:animation-core:1.6.1
androidx.compose.foundation:foundation-android:1.5.4 androidx.compose.animation:animation:1.6.1
androidx.compose.foundation:foundation-layout-android:1.5.4 androidx.compose.foundation:foundation-android:1.6.1
androidx.compose.foundation:foundation-layout:1.5.4 androidx.compose.foundation:foundation-layout-android:1.6.1
androidx.compose.foundation:foundation:1.5.4 androidx.compose.foundation:foundation-layout:1.6.1
androidx.compose.material3:material3:1.1.2 androidx.compose.foundation:foundation:1.6.1
androidx.compose.material:material-icons-core-android:1.5.4 androidx.compose.material3:material3-android:1.2.0
androidx.compose.material:material-icons-core:1.5.4 androidx.compose.material3:material3:1.2.0
androidx.compose.material:material-icons-extended-android:1.5.4 androidx.compose.material:material-icons-core-android:1.6.1
androidx.compose.material:material-icons-extended:1.5.4 androidx.compose.material:material-icons-core:1.6.1
androidx.compose.material:material-ripple-android:1.5.4 androidx.compose.material:material-icons-extended-android:1.6.1
androidx.compose.material:material-ripple:1.5.4 androidx.compose.material:material-icons-extended:1.6.1
androidx.compose.runtime:runtime-android:1.5.4 androidx.compose.material:material-ripple-android:1.6.1
androidx.compose.runtime:runtime-saveable-android:1.5.4 androidx.compose.material:material-ripple:1.6.1
androidx.compose.runtime:runtime-saveable:1.5.4 androidx.compose.runtime:runtime-android:1.6.1
androidx.compose.runtime:runtime:1.5.4 androidx.compose.runtime:runtime-saveable-android:1.6.1
androidx.compose.ui:ui-android:1.5.4 androidx.compose.runtime:runtime-saveable:1.6.1
androidx.compose.ui:ui-geometry-android:1.5.4 androidx.compose.runtime:runtime:1.6.1
androidx.compose.ui:ui-geometry:1.5.4 androidx.compose.ui:ui-android:1.6.1
androidx.compose.ui:ui-graphics-android:1.5.4 androidx.compose.ui:ui-geometry-android:1.6.1
androidx.compose.ui:ui-graphics:1.5.4 androidx.compose.ui:ui-geometry:1.6.1
androidx.compose.ui:ui-text-android:1.5.4 androidx.compose.ui:ui-graphics-android:1.6.1
androidx.compose.ui:ui-text:1.5.4 androidx.compose.ui:ui-graphics:1.6.1
androidx.compose.ui:ui-tooling-preview-android:1.5.4 androidx.compose.ui:ui-text-android:1.6.1
androidx.compose.ui:ui-tooling-preview:1.5.4 androidx.compose.ui:ui-text:1.6.1
androidx.compose.ui:ui-unit-android:1.5.4 androidx.compose.ui:ui-tooling-preview-android:1.6.1
androidx.compose.ui:ui-unit:1.5.4 androidx.compose.ui:ui-tooling-preview:1.6.1
androidx.compose.ui:ui-util-android:1.5.4 androidx.compose.ui:ui-unit-android:1.6.1
androidx.compose.ui:ui-util:1.5.4 androidx.compose.ui:ui-unit:1.6.1
androidx.compose.ui:ui:1.5.4 androidx.compose.ui:ui-util-android:1.6.1
androidx.compose:compose-bom:2023.10.01 androidx.compose.ui:ui-util:1.6.1
androidx.compose.ui:ui:1.6.1
androidx.compose:compose-bom:2024.02.00
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.12.0 androidx.core:core-ktx:1.12.0
androidx.core:core:1.12.0 androidx.core:core:1.12.0
androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.0.0 androidx.customview:customview:1.0.0
androidx.emoji2:emoji2:1.4.0 androidx.emoji2:emoji2:1.3.0
androidx.exifinterface:exifinterface:1.3.6 androidx.exifinterface:exifinterface:1.3.6
androidx.fragment:fragment:1.5.1 androidx.fragment:fragment:1.5.1
androidx.interpolator:interpolator:1.0.0 androidx.interpolator:interpolator:1.0.0
@ -70,17 +72,18 @@ androidx.profileinstaller:profileinstaller:1.3.1
androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate-ktx:1.2.1
androidx.savedstate:savedstate:1.2.1 androidx.savedstate:savedstate:1.2.1
androidx.startup:startup-runtime:1.1.1 androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing:1.0.0 androidx.tracing:tracing-ktx:1.3.0-alpha02
androidx.tracing:tracing:1.3.0-alpha02
androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager:viewpager:1.0.0 androidx.viewpager:viewpager:1.0.0
com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.code.findbugs:jsr305:3.0.2 com.google.code.findbugs:jsr305:3.0.2
com.google.dagger:dagger-lint-aar:2.50 com.google.dagger:dagger-lint-aar:2.51
com.google.dagger:dagger:2.50 com.google.dagger:dagger:2.51
com.google.dagger:hilt-android:2.50 com.google.dagger:hilt-android:2.51
com.google.dagger:hilt-core:2.50 com.google.dagger:hilt-core:2.51
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.6.0 com.squareup.okio:okio-jvm:3.6.0
@ -90,10 +93,10 @@ io.coil-kt:coil-compose-base:2.5.0
io.coil-kt:coil-compose:2.5.0 io.coil-kt:coil-compose:2.5.0
io.coil-kt:coil:2.5.0 io.coil-kt:coil:2.5.0
javax.inject:javax.inject:1 javax.inject:javax.inject:1
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.21 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10
org.jetbrains.kotlin:kotlin-stdlib:1.9.21 org.jetbrains.kotlin:kotlin-stdlib:1.9.22
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3

@ -45,7 +45,7 @@ android {
debug { debug {
applicationIdSuffix = NiaBuildType.DEBUG.applicationIdSuffix applicationIdSuffix = NiaBuildType.DEBUG.applicationIdSuffix
} }
val release = getByName("release") { release {
isMinifyEnabled = true isMinifyEnabled = true
applicationIdSuffix = NiaBuildType.RELEASE.applicationIdSuffix applicationIdSuffix = NiaBuildType.RELEASE.applicationIdSuffix
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@ -57,17 +57,6 @@ android {
// Ensure Baseline Profile is fresh for release builds. // Ensure Baseline Profile is fresh for release builds.
baselineProfile.automaticGenerationDuringBuild = true baselineProfile.automaticGenerationDuringBuild = true
} }
create("benchmark") {
// Enable all the optimizations from release build through initWith(release).
initWith(release)
matchingFallbacks.add("release")
// Debug key signing is available to everyone.
signingConfig = signingConfigs.getByName("debug")
// Only use benchmark proguard rules
proguardFiles("benchmark-rules.pro")
isMinifyEnabled = true
applicationIdSuffix = NiaBuildType.BENCHMARK.applicationIdSuffix
}
} }
packaging { packaging {
@ -104,6 +93,7 @@ dependencies {
implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.material3.windowSizeClass)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)
@ -123,10 +113,12 @@ dependencies {
testDemoImplementation(libs.robolectric) testDemoImplementation(libs.robolectric)
testDemoImplementation(libs.roborazzi) testDemoImplementation(libs.roborazzi)
testDemoImplementation(projects.core.screenshotTesting)
androidTestImplementation(projects.core.testing) androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.dataTest) androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest) androidTestImplementation(projects.core.datastoreTest)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.accompanist.testharness) androidTestImplementation(libs.accompanist.testharness)
androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.hilt.android.testing)

@ -1,7 +1,7 @@
androidx.activity:activity-compose:1.8.0 androidx.activity:activity-compose:1.8.0
androidx.activity:activity-ktx:1.8.0 androidx.activity:activity-ktx:1.8.0
androidx.activity:activity:1.8.0 androidx.activity:activity:1.8.0
androidx.annotation:annotation-experimental:1.3.1 androidx.annotation:annotation-experimental:1.4.0
androidx.annotation:annotation-jvm:1.7.0 androidx.annotation:annotation-jvm:1.7.0
androidx.annotation:annotation:1.7.0 androidx.annotation:annotation:1.7.0
androidx.appcompat:appcompat-resources:1.6.1 androidx.appcompat:appcompat-resources:1.6.1
@ -10,44 +10,47 @@ 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.6.0 androidx.browser:browser:1.6.0
androidx.collection:collection-jvm:1.3.0 androidx.collection:collection-jvm:1.4.0
androidx.collection:collection-ktx:1.3.0 androidx.collection:collection-ktx:1.4.0
androidx.collection:collection:1.3.0 androidx.collection:collection:1.4.0
androidx.compose.animation:animation-android:1.5.4 androidx.compose.animation:animation-android:1.6.1
androidx.compose.animation:animation-core-android:1.5.4 androidx.compose.animation:animation-core-android:1.6.1
androidx.compose.animation:animation-core:1.5.4 androidx.compose.animation:animation-core:1.6.1
androidx.compose.animation:animation:1.5.4 androidx.compose.animation:animation:1.6.1
androidx.compose.foundation:foundation-android:1.5.4 androidx.compose.foundation:foundation-android:1.6.1
androidx.compose.foundation:foundation-layout-android:1.5.4 androidx.compose.foundation:foundation-layout-android:1.6.1
androidx.compose.foundation:foundation-layout:1.5.4 androidx.compose.foundation:foundation-layout:1.6.1
androidx.compose.foundation:foundation:1.5.4 androidx.compose.foundation:foundation:1.6.1
androidx.compose.material3:material3-window-size-class:1.1.2 androidx.compose.material3:material3-android:1.2.0
androidx.compose.material3:material3:1.1.2 androidx.compose.material3:material3-window-size-class-android:1.2.0
androidx.compose.material:material-icons-core-android:1.5.4 androidx.compose.material3:material3-window-size-class:1.2.0
androidx.compose.material:material-icons-core:1.5.4 androidx.compose.material3:material3:1.2.0
androidx.compose.material:material-icons-extended-android:1.5.4 androidx.compose.material:material-icons-core-android:1.6.1
androidx.compose.material:material-icons-extended:1.5.4 androidx.compose.material:material-icons-core:1.6.1
androidx.compose.material:material-ripple-android:1.5.4 androidx.compose.material:material-icons-extended-android:1.6.1
androidx.compose.material:material-ripple:1.5.4 androidx.compose.material:material-icons-extended:1.6.1
androidx.compose.runtime:runtime-android:1.5.4 androidx.compose.material:material-ripple-android:1.6.1
androidx.compose.runtime:runtime-saveable-android:1.5.4 androidx.compose.material:material-ripple:1.6.1
androidx.compose.runtime:runtime-saveable:1.5.4 androidx.compose.runtime:runtime-android:1.6.1
androidx.compose.runtime:runtime:1.5.4 androidx.compose.runtime:runtime-saveable-android:1.6.1
androidx.compose.ui:ui-android:1.5.4 androidx.compose.runtime:runtime-saveable:1.6.1
androidx.compose.ui:ui-geometry-android:1.5.4 androidx.compose.runtime:runtime-tracing:1.0.0-beta01
androidx.compose.ui:ui-geometry:1.5.4 androidx.compose.runtime:runtime:1.6.1
androidx.compose.ui:ui-graphics-android:1.5.4 androidx.compose.ui:ui-android:1.6.1
androidx.compose.ui:ui-graphics:1.5.4 androidx.compose.ui:ui-geometry-android:1.6.1
androidx.compose.ui:ui-text-android:1.5.4 androidx.compose.ui:ui-geometry:1.6.1
androidx.compose.ui:ui-text:1.5.4 androidx.compose.ui:ui-graphics-android:1.6.1
androidx.compose.ui:ui-tooling-preview-android:1.5.4 androidx.compose.ui:ui-graphics:1.6.1
androidx.compose.ui:ui-tooling-preview:1.5.4 androidx.compose.ui:ui-text-android:1.6.1
androidx.compose.ui:ui-unit-android:1.5.4 androidx.compose.ui:ui-text:1.6.1
androidx.compose.ui:ui-unit:1.5.4 androidx.compose.ui:ui-tooling-preview-android:1.6.1
androidx.compose.ui:ui-util-android:1.5.4 androidx.compose.ui:ui-tooling-preview:1.6.1
androidx.compose.ui:ui-util:1.5.4 androidx.compose.ui:ui-unit-android:1.6.1
androidx.compose.ui:ui:1.5.4 androidx.compose.ui:ui-unit:1.6.1
androidx.compose:compose-bom:2023.10.01 androidx.compose.ui:ui-util-android:1.6.1
androidx.compose.ui:ui-util:1.6.1
androidx.compose.ui:ui:1.6.1
androidx.compose:compose-bom:2024.02.00
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.12.0 androidx.core:core-ktx:1.12.0
androidx.core:core-splashscreen:1.0.1 androidx.core:core-splashscreen:1.0.1
@ -61,8 +64,8 @@ androidx.datastore:datastore-preferences:1.0.0
androidx.datastore:datastore:1.0.0 androidx.datastore:datastore:1.0.0
androidx.documentfile:documentfile:1.0.0 androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.0.0 androidx.drawerlayout:drawerlayout:1.0.0
androidx.emoji2:emoji2-views-helper:1.4.0 androidx.emoji2:emoji2-views-helper:1.3.0
androidx.emoji2:emoji2:1.4.0 androidx.emoji2:emoji2:1.3.0
androidx.exifinterface:exifinterface:1.3.6 androidx.exifinterface:exifinterface:1.3.6
androidx.fragment:fragment:1.5.1 androidx.fragment:fragment:1.5.1
androidx.hilt:hilt-common:1.1.0 androidx.hilt:hilt-common:1.1.0
@ -71,19 +74,20 @@ androidx.hilt:hilt-navigation:1.0.0
androidx.hilt:hilt-work:1.1.0 androidx.hilt:hilt-work:1.1.0
androidx.interpolator:interpolator:1.0.0 androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.6.2 androidx.lifecycle:lifecycle-common-java8:2.7.0
androidx.lifecycle:lifecycle-common:2.6.2 androidx.lifecycle:lifecycle-common:2.7.0
androidx.lifecycle:lifecycle-livedata-core:2.6.2 androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0
androidx.lifecycle:lifecycle-livedata:2.6.2 androidx.lifecycle:lifecycle-livedata-core:2.7.0
androidx.lifecycle:lifecycle-process:2.6.2 androidx.lifecycle:lifecycle-livedata:2.7.0
androidx.lifecycle:lifecycle-runtime-compose:2.6.2 androidx.lifecycle:lifecycle-process:2.7.0
androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 androidx.lifecycle:lifecycle-runtime-compose:2.7.0
androidx.lifecycle:lifecycle-runtime:2.6.2 androidx.lifecycle:lifecycle-runtime-ktx:2.7.0
androidx.lifecycle:lifecycle-service:2.6.2 androidx.lifecycle:lifecycle-runtime:2.7.0
androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 androidx.lifecycle:lifecycle-service:2.7.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2 androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0
androidx.lifecycle:lifecycle-viewmodel:2.6.2 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0
androidx.lifecycle:lifecycle-viewmodel:2.7.0
androidx.loader:loader:1.0.0 androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-alpha04 androidx.metrics:metrics-performance:1.0.0-alpha04
@ -105,8 +109,9 @@ androidx.savedstate:savedstate:1.2.1
androidx.sqlite:sqlite-framework:2.4.0 androidx.sqlite:sqlite-framework:2.4.0
androidx.sqlite:sqlite:2.4.0 androidx.sqlite:sqlite:2.4.0
androidx.startup:startup-runtime:1.1.1 androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing-ktx:1.1.0 androidx.tracing:tracing-ktx:1.3.0-alpha02
androidx.tracing:tracing:1.1.0 androidx.tracing:tracing-perfetto:1.0.0
androidx.tracing:tracing:1.3.0-alpha02
androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.versionedparcelable:versionedparcelable:1.1.1
@ -134,10 +139,10 @@ com.google.android.gms:play-services-oss-licenses:17.0.1
com.google.android.gms:play-services-stats:17.0.2 com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.0.2 com.google.android.gms:play-services-tasks:18.0.2
com.google.code.findbugs:jsr305:3.0.2 com.google.code.findbugs:jsr305:3.0.2
com.google.dagger:dagger-lint-aar:2.50 com.google.dagger:dagger-lint-aar:2.51
com.google.dagger:dagger:2.50 com.google.dagger:dagger:2.51
com.google.dagger:hilt-android:2.50 com.google.dagger:hilt-android:2.51
com.google.dagger:hilt-core:2.50 com.google.dagger:hilt-core:2.51
com.google.errorprone:error_prone_annotations:2.11.0 com.google.errorprone:error_prone_annotations:2.11.0
com.google.firebase:firebase-abt:21.1.1 com.google.firebase:firebase-abt:21.1.1
com.google.firebase:firebase-analytics-ktx:21.4.0 com.google.firebase:firebase-analytics-ktx:21.4.0
@ -168,8 +173,8 @@ com.google.guava:failureaccess:1.0.1
com.google.guava:guava:31.1-android com.google.guava:guava:31.1-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:1.3 com.google.j2objc:j2objc-annotations:1.3
com.google.protobuf:protobuf-javalite:3.24.4 com.google.protobuf:protobuf-javalite:3.25.2
com.google.protobuf:protobuf-kotlin-lite:3.24.4 com.google.protobuf:protobuf-kotlin-lite:3.25.2
com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0 com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0
com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:logging-interceptor:4.12.0
com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okhttp3:okhttp:4.12.0
@ -185,10 +190,10 @@ io.github.aakira:napier-android:1.4.1
io.github.aakira:napier:1.4.1 io.github.aakira:napier:1.4.1
javax.inject:javax.inject:1 javax.inject:javax.inject:1
org.checkerframework:checker-qual:3.12.0 org.checkerframework:checker-qual:3.12.0
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.21 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10
org.jetbrains.kotlin:kotlin-stdlib:1.9.21 org.jetbrains.kotlin:kotlin-stdlib:1.9.22
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3

@ -19,14 +19,17 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.testharness.TestHarness import com.google.accompanist.testharness.TestHarness
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
@ -81,6 +84,9 @@ class NavigationUiTest {
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor
@Before @Before
fun setup() { fun setup() {
hiltRule.inject() hiltRule.inject()
@ -91,13 +97,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(400.dp, 400.dp)) { TestHarness(size = DpSize(400.dp, 400.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -111,13 +111,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(610.dp, 400.dp)) { TestHarness(size = DpSize(610.dp, 400.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -131,13 +125,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(900.dp, 400.dp)) { TestHarness(size = DpSize(900.dp, 400.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -151,13 +139,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(400.dp, 500.dp)) { TestHarness(size = DpSize(400.dp, 500.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -171,13 +153,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(610.dp, 500.dp)) { TestHarness(size = DpSize(610.dp, 500.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -191,13 +167,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(900.dp, 500.dp)) { TestHarness(size = DpSize(900.dp, 500.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -211,13 +181,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(400.dp, 1000.dp)) { TestHarness(size = DpSize(400.dp, 1000.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -231,13 +195,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(610.dp, 1000.dp)) { TestHarness(size = DpSize(610.dp, 1000.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -251,13 +209,7 @@ class NavigationUiTest {
composeTestRule.setContent { composeTestRule.setContent {
TestHarness(size = DpSize(900.dp, 1000.dp)) { TestHarness(size = DpSize(900.dp, 1000.dp)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaApp(fakeAppState(maxWidth, maxHeight))
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(maxWidth, maxHeight),
),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }
@ -265,4 +217,12 @@ class NavigationUiTest {
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
} }
@Composable
private fun fakeAppState(maxWidth: Dp, maxHeight: Dp) = rememberNiaAppState(
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(maxWidth, maxHeight)),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
} }

@ -34,10 +34,12 @@ import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNe
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.TimeZone
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -59,6 +61,8 @@ class NiaAppStateTest {
// Create the test dependencies. // Create the test dependencies.
private val networkMonitor = TestNetworkMonitor() private val networkMonitor = TestNetworkMonitor()
private val timeZoneMonitor = TestTimeZoneMonitor()
private val userNewsResourceRepository = private val userNewsResourceRepository =
CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository()) CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository())
@ -78,6 +82,7 @@ class NiaAppStateTest {
windowSizeClass = getCompactWindowClass(), windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
) )
} }
@ -100,6 +105,7 @@ class NiaAppStateTest {
windowSizeClass = getCompactWindowClass(), windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
) )
} }
@ -118,6 +124,7 @@ class NiaAppStateTest {
windowSizeClass = getCompactWindowClass(), windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
) )
} }
@ -134,6 +141,7 @@ class NiaAppStateTest {
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)), windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
) )
} }
@ -150,6 +158,7 @@ class NiaAppStateTest {
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)), windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
) )
} }
@ -166,6 +175,7 @@ class NiaAppStateTest {
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)), windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
) )
} }
@ -177,6 +187,27 @@ class NiaAppStateTest {
) )
} }
@Test
fun niaAppState_differentTZ_withTimeZoneMonitorChange() = runTest(UnconfinedTestDispatcher()) {
composeTestRule.setContent {
state = NiaAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
}
val changedTz = TimeZone.of("Europe/Prague")
backgroundScope.launch { state.currentTimeZone.collect() }
timeZoneMonitor.setTimeZone(changedTz)
assertEquals(
changedTz,
state.currentTimeZone.value,
)
}
private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(500.dp, 300.dp)) private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(500.dp, 300.dp))
} }

@ -33,6 +33,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.metrics.performance.JankStats import androidx.metrics.performance.JankStats
@ -42,10 +43,13 @@ import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig import com.google.samples.apps.nowinandroid.core.model.data.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.ui.LocalTimeZone
import com.google.samples.apps.nowinandroid.ui.NiaApp import com.google.samples.apps.nowinandroid.ui.NiaApp
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -67,6 +71,9 @@ class MainActivity : ComponentActivity() {
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor
@Inject @Inject
lateinit var analyticsHelper: AnalyticsHelper lateinit var analyticsHelper: AnalyticsHelper
@ -126,17 +133,25 @@ class MainActivity : ComponentActivity() {
onDispose {} onDispose {}
} }
CompositionLocalProvider(LocalAnalyticsHelper provides analyticsHelper) { val appState = rememberNiaAppState(
windowSizeClass = calculateWindowSizeClass(this),
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
CompositionLocalProvider(
LocalAnalyticsHelper provides analyticsHelper,
LocalTimeZone provides currentTimeZone,
) {
NiaTheme( NiaTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(uiState), androidTheme = shouldUseAndroidTheme(uiState),
disableDynamicTheming = shouldDisableDynamicTheming(uiState), disableDynamicTheming = shouldDisableDynamicTheming(uiState),
) { ) {
NiaApp( NiaApp(appState)
networkMonitor = networkMonitor,
windowSizeClass = calculateWindowSizeClass(this),
userNewsResourceRepository = userNewsResourceRepository,
)
} }
} }
} }

@ -23,7 +23,6 @@ import com.google.samples.apps.nowinandroid.sync.initializers.Sync
import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
/** /**
* [Application] class for NiA * [Application] class for NiA
@ -31,7 +30,7 @@ import javax.inject.Provider
@HiltAndroidApp @HiltAndroidApp
class NiaApplication : Application(), ImageLoaderFactory { class NiaApplication : Application(), ImageLoaderFactory {
@Inject @Inject
lateinit var imageLoader: Provider<ImageLoader> lateinit var imageLoader: dagger.Lazy<ImageLoader>
@Inject @Inject
lateinit var profileVerifierLogger: ProfileVerifierLogger lateinit var profileVerifierLogger: ProfileVerifierLogger

@ -39,7 +39,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.SnackbarResult.ActionPerformed
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -62,8 +61,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
@ -85,16 +82,7 @@ import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
ExperimentalComposeUiApi::class, ExperimentalComposeUiApi::class,
) )
@Composable @Composable
fun NiaApp( fun NiaApp(appState: NiaAppState) {
windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
appState: NiaAppState = rememberNiaAppState(
networkMonitor = networkMonitor,
windowSizeClass = windowSizeClass,
userNewsResourceRepository = userNewsResourceRepository,
),
) {
val shouldShowGradientBackground = val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
var showSettingsDialog by rememberSaveable { mutableStateOf(false) } var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
@ -195,13 +183,16 @@ fun NiaApp(
) )
} }
NiaNavHost(appState = appState, onShowSnackbar = { message, action -> NiaNavHost(
snackbarHostState.showSnackbar( appState = appState,
message = message, onShowSnackbar = { message, action ->
actionLabel = action, snackbarHostState.showSnackbar(
duration = Short, message = message,
) == ActionPerformed actionLabel = action,
}) duration = Short,
) == ActionPerformed
},
)
} }
// TODO: We may want to add padding or spacer when the snackbar is shown so that // TODO: We may want to add padding or spacer when the snackbar is shown so that

@ -32,6 +32,7 @@ import androidx.navigation.navOptions
import androidx.tracing.trace import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BOOKMARKS_ROUTE import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BOOKMARKS_ROUTE
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
@ -50,12 +51,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.datetime.TimeZone
@Composable @Composable
fun rememberNiaAppState( fun rememberNiaAppState(
windowSizeClass: WindowSizeClass, windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
coroutineScope: CoroutineScope = rememberCoroutineScope(), coroutineScope: CoroutineScope = rememberCoroutineScope(),
navController: NavHostController = rememberNavController(), navController: NavHostController = rememberNavController(),
): NiaAppState { ): NiaAppState {
@ -66,13 +69,15 @@ fun rememberNiaAppState(
windowSizeClass, windowSizeClass,
networkMonitor, networkMonitor,
userNewsResourceRepository, userNewsResourceRepository,
timeZoneMonitor,
) { ) {
NiaAppState( NiaAppState(
navController, navController = navController,
coroutineScope, coroutineScope = coroutineScope,
windowSizeClass, windowSizeClass = windowSizeClass,
networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
) )
} }
} }
@ -80,10 +85,11 @@ fun rememberNiaAppState(
@Stable @Stable
class NiaAppState( class NiaAppState(
val navController: NavHostController, val navController: NavHostController,
val coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
val windowSizeClass: WindowSizeClass, val windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
) { ) {
val currentDestination: NavDestination? val currentDestination: NavDestination?
@Composable get() = navController @Composable get() = navController
@ -127,12 +133,20 @@ class NiaAppState(
FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } },
BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } },
) )
}.stateIn( }
.stateIn(
coroutineScope, coroutineScope,
SharingStarted.WhileSubscribed(5_000), SharingStarted.WhileSubscribed(5_000),
initialValue = emptySet(), initialValue = emptySet(),
) )
val currentTimeZone = timeZoneMonitor.currentTimeZone
.stateIn(
coroutineScope,
SharingStarted.WhileSubscribed(5_000),
TimeZone.currentSystemDefault(),
)
/** /**
* UI logic for navigating to a top level destination in the app. Top level destinations have * UI logic for navigating to a top level destination in the app. Top level destinations have
* only one copy of the destination of the back stack, and save and restore state whenever you * only one copy of the destination of the back stack, and save and restore state whenever you

@ -37,6 +37,8 @@ import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepositor
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.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.BindValue
@ -93,6 +95,9 @@ class NiaAppScreenSizesScreenshotTests {
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor
@Inject @Inject
lateinit var userDataRepository: UserDataRepository lateinit var userDataRepository: UserDataRepository
@ -140,13 +145,17 @@ class NiaAppScreenSizesScreenshotTests {
) { ) {
TestHarness(size = DpSize(width, height)) { TestHarness(size = DpSize(width, height)) {
BoxWithConstraints { BoxWithConstraints {
NiaApp( NiaTheme {
windowSizeClass = WindowSizeClass.calculateFromSize( val fakeAppState = rememberNiaAppState(
DpSize(maxWidth, maxHeight), windowSizeClass = WindowSizeClass.calculateFromSize(
), DpSize(maxWidth, maxHeight),
networkMonitor = networkMonitor, ),
userNewsResourceRepository = userNewsResourceRepository, networkMonitor = networkMonitor,
) userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
NiaApp(fakeAppState)
}
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 103 KiB

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import com.google.samples.apps.nowinandroid.NiaBuildType
import com.google.samples.apps.nowinandroid.configureFlavors import com.google.samples.apps.nowinandroid.configureFlavors
plugins { plugins {
@ -35,23 +34,6 @@ android {
buildConfig = true buildConfig = true
} }
buildTypes {
// This benchmark buildType is used for benchmarking, and should function like your
// release build (for example, with minification on). It's signed with a debug key
// for easy local/CI testing.
create("benchmark") {
// Keep the build type debuggable so we can attach a debugger if needed.
isDebuggable = true
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
buildConfigField(
"String",
"APP_BUILD_TYPE_SUFFIX",
"\"${NiaBuildType.BENCHMARK.applicationIdSuffix ?: ""}\""
)
}
}
// Use the same flavor dimensions as the application to allow generating Baseline Profiles on prod, // Use the same flavor dimensions as the application to allow generating Baseline Profiles on prod,
// which is more close to what will be shipped to users (no fake data), but has ability to run the // which is more close to what will be shipped to users (no fake data), but has ability to run the
// benchmarks on demo, so we benchmark on stable data. // benchmarks on demo, so we benchmark on stable data.

@ -30,7 +30,6 @@ import java.io.ByteArrayOutputStream
val PACKAGE_NAME = buildString { val PACKAGE_NAME = buildString {
append("com.google.samples.apps.nowinandroid") append("com.google.samples.apps.nowinandroid")
append(BuildConfig.APP_FLAVOR_SUFFIX) append(BuildConfig.APP_FLAVOR_SUFFIX)
append(BuildConfig.APP_BUILD_TYPE_SUFFIX)
} }
fun UiDevice.flingElementDownUp(element: UiObject2) { fun UiDevice.flingElementDownUp(element: UiObject2) {

@ -38,6 +38,9 @@ class ScrollForYouFeedBenchmark {
@Test @Test
fun scrollFeedCompilationBaselineProfile() = scrollFeed(CompilationMode.Partial()) fun scrollFeedCompilationBaselineProfile() = scrollFeed(CompilationMode.Partial())
@Test
fun scrollFeedCompilationFull() = scrollFeed(CompilationMode.Full())
private fun scrollFeed(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( private fun scrollFeed(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME, packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric()), metrics = listOf(FrameTimingMetric()),

@ -39,6 +39,8 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
extensions.configure<ApplicationExtension> { extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 34
@Suppress("UnstableApiUsage")
testOptions.animationsDisabled = true
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
} }
extensions.configure<ApplicationAndroidComponentsExtension> { extensions.configure<ApplicationAndroidComponentsExtension> {

@ -34,6 +34,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
testInstrumentationRunner = testInstrumentationRunner =
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
} }
testOptions.animationsDisabled = true
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
} }
@ -44,6 +45,9 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
add("implementation", libs.findLibrary("androidx.tracing.ktx").get())
add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get())
} }
} }
} }

@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests
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.configure import org.gradle.kotlin.dsl.configure
@ -39,6 +40,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
extensions.configure<LibraryExtension> { extensions.configure<LibraryExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 34
testOptions.animationsDisabled = true
configureFlavors(this) configureFlavors(this)
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
// The resource prefix is derived from the module name, // The resource prefix is derived from the module name,
@ -51,6 +53,8 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
} }
dependencies { dependencies {
add("testImplementation", kotlin("test")) add("testImplementation", kotlin("test"))
add("implementation", libs.findLibrary("androidx.tracing.ktx").get())
} }
} }
} }

@ -42,6 +42,8 @@ internal fun Project.configureAndroidCompose(
val bom = libs.findLibrary("androidx-compose-bom").get() val bom = libs.findLibrary("androidx-compose-bom").get()
add("implementation", platform(bom)) add("implementation", platform(bom))
add("androidTestImplementation", platform(bom)) add("androidTestImplementation", platform(bom))
add("implementation", libs.findLibrary("androidx-compose-ui-tooling-preview").get())
add("debugImplementation", libs.findLibrary("androidx-compose-ui-tooling").get())
} }
testOptions { testOptions {
@ -54,7 +56,8 @@ internal fun Project.configureAndroidCompose(
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
freeCompilerArgs = freeCompilerArgs + buildComposeMetricsParameters() freeCompilerArgs += buildComposeMetricsParameters()
freeCompilerArgs += stabilityConfiguration()
} }
} }
} }
@ -84,3 +87,8 @@ private fun Project.buildComposeMetricsParameters(): List<String> {
} }
return metricParameters.toList() return metricParameters.toList()
} }
private fun Project.stabilityConfiguration() = listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=${project.rootDir.absolutePath}/compose_compiler_config.conf",
)

@ -22,5 +22,4 @@ package com.google.samples.apps.nowinandroid
enum class NiaBuildType(val applicationIdSuffix: String? = null) { enum class NiaBuildType(val applicationIdSuffix: String? = null) {
DEBUG(".debug"), DEBUG(".debug"),
RELEASE, RELEASE,
BENCHMARK(".benchmark")
} }

@ -0,0 +1,6 @@
// 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.
// For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file
java.time.ZoneId
java.time.ZoneOffset

@ -38,7 +38,7 @@ class ResultKtTest {
when (val errorResult = awaitItem()) { when (val errorResult = awaitItem()) {
is Result.Error -> assertEquals( is Result.Error -> assertEquals(
"Test Done", "Test Done",
errorResult.exception?.message, errorResult.exception.message,
) )
Result.Loading, Result.Loading,
is Result.Success, is Result.Success,

@ -0,0 +1,27 @@
/*
* 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.data.test
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.datetime.TimeZone
import javax.inject.Inject
class DefaultZoneIdTimeZoneMonitor @Inject constructor() : TimeZoneMonitor {
override val currentTimeZone: Flow<TimeZone> = flowOf(TimeZone.of("Europe/Warsaw"))
}

@ -22,12 +22,13 @@ import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRep
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
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.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeNewsRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeNewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeRecentSearchRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeRecentSearchRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeSearchContentsRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeSearchContentsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeTopicsRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeTopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeUserDataRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
@ -38,7 +39,7 @@ import dagger.hilt.testing.TestInstallIn
components = [SingletonComponent::class], components = [SingletonComponent::class],
replaces = [DataModule::class], replaces = [DataModule::class],
) )
interface TestDataModule { internal interface TestDataModule {
@Binds @Binds
fun bindsTopicRepository( fun bindsTopicRepository(
fakeTopicsRepository: FakeTopicsRepository, fakeTopicsRepository: FakeTopicsRepository,
@ -68,4 +69,7 @@ interface TestDataModule {
fun bindsNetworkMonitor( fun bindsNetworkMonitor(
networkMonitor: AlwaysOnlineNetworkMonitor, networkMonitor: AlwaysOnlineNetworkMonitor,
): NetworkMonitor ): NetworkMonitor
@Binds
fun binds(impl: DefaultZoneIdTimeZoneMonitor): TimeZoneMonitor
} }

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.data.repository.fake 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.asEntity
@ -39,7 +39,7 @@ import javax.inject.Inject
* This allows us to run the app with fake data, without needing an internet connection or working * This allows us to run the app with fake data, without needing an internet connection or working
* backend. * backend.
*/ */
class FakeNewsRepository @Inject constructor( internal class FakeNewsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val datasource: FakeNiaNetworkDataSource, private val datasource: FakeNiaNetworkDataSource,
) : NewsRepository { ) : NewsRepository {

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.data.repository.fake package com.google.samples.apps.nowinandroid.core.data.test.repository
import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery
import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository
@ -25,7 +25,7 @@ import javax.inject.Inject
/** /**
* Fake implementation of the [RecentSearchRepository] * Fake implementation of the [RecentSearchRepository]
*/ */
class FakeRecentSearchRepository @Inject constructor() : RecentSearchRepository { internal class FakeRecentSearchRepository @Inject constructor() : RecentSearchRepository {
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) = Unit override suspend fun insertOrReplaceRecentSearch(searchQuery: String) = Unit
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> = override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.data.repository.fake package com.google.samples.apps.nowinandroid.core.data.test.repository
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
@ -25,7 +25,7 @@ import javax.inject.Inject
/** /**
* Fake implementation of the [SearchContentsRepository] * Fake implementation of the [SearchContentsRepository]
*/ */
class FakeSearchContentsRepository @Inject constructor() : SearchContentsRepository { internal class FakeSearchContentsRepository @Inject constructor() : SearchContentsRepository {
override suspend fun populateFtsData() = Unit override suspend fun populateFtsData() = Unit
override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf() override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf()

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.data.repository.fake 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.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
@ -36,7 +36,7 @@ import javax.inject.Inject
* This allows us to run the app with fake data, without needing an internet connection or working * This allows us to run the app with fake data, without needing an internet connection or working
* backend. * backend.
*/ */
class FakeTopicsRepository @Inject constructor( internal class FakeTopicsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val datasource: FakeNiaNetworkDataSource, private val datasource: FakeNiaNetworkDataSource,
) : TopicsRepository { ) : TopicsRepository {

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.core.data.repository.fake package com.google.samples.apps.nowinandroid.core.data.test.repository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
@ -30,7 +30,7 @@ import javax.inject.Inject
* This allows us to run the app with fake data, without needing an internet connection or working * This allows us to run the app with fake data, without needing an internet connection or working
* backend. * backend.
*/ */
class FakeUserDataRepository @Inject constructor( internal class FakeUserDataRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource, private val niaPreferencesDataSource: NiaPreferencesDataSource,
) : UserDataRepository { ) : UserDataRepository {

@ -28,6 +28,8 @@ import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepositor
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ConnectivityManagerNetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.ConnectivityManagerNetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneBroadcastMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -66,4 +68,7 @@ abstract class DataModule {
internal abstract fun bindsNetworkMonitor( internal abstract fun bindsNetworkMonitor(
networkMonitor: ConnectivityManagerNetworkMonitor, networkMonitor: ConnectivityManagerNetworkMonitor,
): NetworkMonitor ): NetworkMonitor
@Binds
internal abstract fun binds(impl: TimeZoneBroadcastMonitor): TimeZoneMonitor
} }

@ -0,0 +1,107 @@
/*
* 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.data.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.tracing.trace
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.di.ApplicationScope
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.shareIn
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toKotlinTimeZone
import java.time.ZoneId
import javax.inject.Inject
import javax.inject.Singleton
/**
* Utility for reporting current timezone the device has set.
* It always emits at least once with default setting and then for each TZ change.
*/
interface TimeZoneMonitor {
val currentTimeZone: Flow<TimeZone>
}
@Singleton
internal class TimeZoneBroadcastMonitor @Inject constructor(
@ApplicationContext private val context: Context,
@ApplicationScope appScope: CoroutineScope,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
) : TimeZoneMonitor {
override val currentTimeZone: SharedFlow<TimeZone> =
callbackFlow {
// Send the default time zone first.
trySend(TimeZone.currentSystemDefault())
// Registers BroadcastReceiver for the TimeZone changes
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_TIMEZONE_CHANGED) return
val zoneIdFromIntent = if (VERSION.SDK_INT < VERSION_CODES.R) {
null
} else {
// Starting Android R we also get the new TimeZone.
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.let { timeZoneId ->
// We need to convert it from java.util.Timezone to java.time.ZoneId
val zoneId = ZoneId.of(timeZoneId, ZoneId.SHORT_IDS)
// Convert to kotlinx.datetime.TimeZone
zoneId.toKotlinTimeZone()
}
}
// If there isn't a zoneId in the intent, fallback to the systemDefault, which should also reflect the change
trySend(zoneIdFromIntent ?: TimeZone.currentSystemDefault())
}
}
trace("TimeZoneBroadcastReceiver.register") {
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
}
// Send here again, because registering the Broadcast Receiver can take up to several milliseconds.
// This way, we can reduce the likelihood that a TZ change wouldn't be caught with the Broadcast Receiver.
trySend(TimeZone.currentSystemDefault())
awaitClose {
context.unregisterReceiver(receiver)
}
}
// We use to prevent multiple emissions of the same type, because we use trySend multiple times.
.distinctUntilChanged()
.conflate()
.flowOn(ioDispatcher)
// Sharing the callback to prevent multiple BroadcastReceivers being registered
.shareIn(appScope, SharingStarted.WhileSubscribed(5_000), 1)
}

@ -92,21 +92,6 @@ class TestNewsResourceDao : NewsResourceDao {
result.map { it.entity.id } result.map { it.entity.id }
} }
override suspend fun insertOrIgnoreNewsResources(
entities: List<NewsResourceEntity>,
): List<Long> {
entitiesStateFlow.update { oldValues ->
// Old values come first so new values don't overwrite them
(oldValues + entities)
.distinctBy(NewsResourceEntity::id)
.sortedWith(
compareBy(NewsResourceEntity::publishDate).reversed(),
)
}
// Assume no conflicts on insert
return entities.map { it.id.toLong() }
}
override suspend fun upsertNewsResources(newsResourceEntities: List<NewsResourceEntity>) { override suspend fun upsertNewsResources(newsResourceEntities: List<NewsResourceEntity>) {
entitiesStateFlow.update { oldValues -> entitiesStateFlow.update { oldValues ->
// New values come first so they overwrite old values // New values come first so they overwrite old values

@ -96,12 +96,6 @@ interface NewsResourceDao {
filterNewsIds: Set<String> = emptySet(), filterNewsIds: Set<String> = emptySet(),
): Flow<List<String>> ): Flow<List<String>>
/**
* Inserts [entities] into the db if they don't exist, and ignores those that do
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnoreNewsResources(entities: List<NewsResourceEntity>): List<Long>
/** /**
* Inserts or updates [newsResourceEntities] in the db under the specified primary keys * Inserts or updates [newsResourceEntities] in the db under the specified primary keys
*/ */

@ -35,11 +35,8 @@ dependencies {
api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material.iconsExtended)
api(libs.androidx.compose.material3) api(libs.androidx.compose.material3)
api(libs.androidx.compose.runtime) api(libs.androidx.compose.runtime)
api(libs.androidx.compose.ui.tooling.preview)
api(libs.androidx.compose.ui.util) api(libs.androidx.compose.ui.util)
debugApi(libs.androidx.compose.ui.tooling)
implementation(libs.coil.kt.compose) implementation(libs.coil.kt.compose)
implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava") implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava")
@ -48,6 +45,7 @@ dependencies {
testImplementation(libs.hilt.android.testing) testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric) testImplementation(libs.robolectric)
testImplementation(libs.roborazzi) testImplementation(libs.roborazzi)
testImplementation(projects.core.screenshotTesting)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)
androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.androidx.compose.ui.test)

@ -285,16 +285,6 @@ fun NiaOutlinedButtonPreview() {
} }
} }
@ThemePreviews
@Composable
fun NiaButtonPreview2() {
NiaTheme {
NiaBackground(modifier = Modifier.size(150.dp, 50.dp)) {
NiaButton(onClick = {}, text = { Text("Test button") })
}
}
}
@ThemePreviews @ThemePreviews
@Composable @Composable
fun NiaButtonLeadingIconPreview() { fun NiaButtonLeadingIconPreview() {

@ -73,6 +73,8 @@ fun NiaFilterChip(
}, },
shape = CircleShape, shape = CircleShape,
border = FilterChipDefaults.filterChipBorder( border = FilterChipDefaults.filterChipBorder(
enabled = enabled,
selected = selected,
borderColor = MaterialTheme.colorScheme.onBackground, borderColor = MaterialTheme.colorScheme.onBackground,
selectedBorderColor = MaterialTheme.colorScheme.onBackground, selectedBorderColor = MaterialTheme.colorScheme.onBackground,
disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy( disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy(

@ -96,8 +96,8 @@ fun NiaLoadingWheel(
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = keyframes { animation = keyframes {
durationMillis = ROTATION_TIME / 2 durationMillis = ROTATION_TIME / 2
progressLineColor at ROTATION_TIME / NUM_OF_LINES / 2 with LinearEasing progressLineColor at ROTATION_TIME / NUM_OF_LINES / 2 using LinearEasing
baseLineColor at ROTATION_TIME / NUM_OF_LINES with LinearEasing baseLineColor at ROTATION_TIME / NUM_OF_LINES using LinearEasing
}, },
repeatMode = RepeatMode.Restart, repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(ROTATION_TIME / NUM_OF_LINES / 2 * index), initialStartOffset = StartOffset(ROTATION_TIME / NUM_OF_LINES / 2 * index),

@ -53,12 +53,12 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
fun RowScope.NiaNavigationBarItem( fun RowScope.NiaNavigationBarItem(
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selectedIcon: @Composable () -> Unit = icon,
enabled: Boolean = true, enabled: Boolean = true,
label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true, alwaysShowLabel: Boolean = true,
icon: @Composable () -> Unit,
selectedIcon: @Composable () -> Unit = icon,
label: @Composable (() -> Unit)? = null,
) { ) {
NavigationBarItem( NavigationBarItem(
selected = selected, selected = selected,
@ -117,12 +117,12 @@ fun NiaNavigationBar(
fun NiaNavigationRailItem( fun NiaNavigationRailItem(
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selectedIcon: @Composable () -> Unit = icon,
enabled: Boolean = true, enabled: Boolean = true,
label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true, alwaysShowLabel: Boolean = true,
icon: @Composable () -> Unit,
selectedIcon: @Composable () -> Unit = icon,
label: @Composable (() -> Unit)? = null,
) { ) {
NavigationRailItem( NavigationRailItem(
selected = selected, selected = selected,
@ -167,7 +167,7 @@ fun NiaNavigationRail(
@ThemePreviews @ThemePreviews
@Composable @Composable
fun NiaNavigationPreview() { fun NiaNavigationBarPreview() {
val items = listOf("For you", "Saved", "Interests") val items = listOf("For you", "Saved", "Interests")
val icons = listOf( val icons = listOf(
NiaIcons.UpcomingBorder, NiaIcons.UpcomingBorder,
@ -205,6 +205,46 @@ fun NiaNavigationPreview() {
} }
} }
@ThemePreviews
@Composable
fun NiaNavigationRailPreview() {
val items = listOf("For you", "Saved", "Interests")
val icons = listOf(
NiaIcons.UpcomingBorder,
NiaIcons.BookmarksBorder,
NiaIcons.Grid3x3,
)
val selectedIcons = listOf(
NiaIcons.Upcoming,
NiaIcons.Bookmarks,
NiaIcons.Grid3x3,
)
NiaTheme {
NiaNavigationRail {
items.forEachIndexed { index, item ->
NiaNavigationRailItem(
icon = {
Icon(
imageVector = icons[index],
contentDescription = item,
)
},
selectedIcon = {
Icon(
imageVector = selectedIcons[index],
contentDescription = item,
)
},
label = { Text(item) },
selected = index == 0,
onClick = { },
)
}
}
}
}
/** /**
* Now in Android navigation default values. * Now in Android navigation default values.
*/ */

@ -91,7 +91,7 @@ fun NiaTabRow(
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface, contentColor = MaterialTheme.colorScheme.onSurface,
indicator = { tabPositions -> indicator = { tabPositions ->
TabRowDefaults.Indicator( TabRowDefaults.SecondaryIndicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]), modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
height = 2.dp, height = 2.dp,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,

@ -34,6 +34,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -77,11 +78,13 @@ fun NiaTopAppBar(
@Preview("Top App Bar") @Preview("Top App Bar")
@Composable @Composable
private fun NiaTopAppBarPreview() { private fun NiaTopAppBarPreview() {
NiaTopAppBar( NiaTheme {
titleRes = android.R.string.untitled, NiaTopAppBar(
navigationIcon = NiaIcons.Search, titleRes = android.R.string.untitled,
navigationIconContentDescription = "Navigation icon", navigationIcon = NiaIcons.Search,
actionIcon = NiaIcons.MoreVert, navigationIconContentDescription = "Navigation icon",
actionIconContentDescription = "Action icon", actionIcon = NiaIcons.MoreVert,
) actionIconContentDescription = "Action icon",
)
}
} }

@ -75,10 +75,10 @@ private const val SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS = 2_000L
*/ */
@Composable @Composable
fun ScrollableState.DraggableScrollbar( fun ScrollableState.DraggableScrollbar(
modifier: Modifier = Modifier,
state: ScrollbarState, state: ScrollbarState,
orientation: Orientation, orientation: Orientation,
onThumbMoved: (Float) -> Unit, onThumbMoved: (Float) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Scrollbar( Scrollbar(
@ -105,9 +105,9 @@ fun ScrollableState.DraggableScrollbar(
*/ */
@Composable @Composable
fun ScrollableState.DecorativeScrollbar( fun ScrollableState.DecorativeScrollbar(
modifier: Modifier = Modifier,
state: ScrollbarState, state: ScrollbarState,
orientation: Orientation, orientation: Orientation,
modifier: Modifier = Modifier,
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Scrollbar( Scrollbar(

@ -195,13 +195,13 @@ internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) {
*/ */
@Composable @Composable
fun Scrollbar( fun Scrollbar(
modifier: Modifier = Modifier,
orientation: Orientation, orientation: Orientation,
state: ScrollbarState, state: ScrollbarState,
minThumbSize: Dp = 40.dp, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null,
thumb: @Composable () -> Unit, minThumbSize: Dp = 40.dp,
onThumbMoved: ((Float) -> Unit)? = null, onThumbMoved: ((Float) -> Unit)? = null,
thumb: @Composable () -> Unit,
) { ) {
// Using Offset.Unspecified and Float.NaN instead of null // Using Offset.Unspecified and Float.NaN instead of null
// to prevent unnecessary boxing of primitives // to prevent unnecessary boxing of primitives

@ -17,6 +17,8 @@
package com.google.samples.apps.nowinandroid.core.designsystem.icon package com.google.samples.apps.nowinandroid.core.designsystem.icon
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.ShortText
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.Bookmarks import androidx.compose.material.icons.outlined.Bookmarks
import androidx.compose.material.icons.outlined.Upcoming import androidx.compose.material.icons.outlined.Upcoming
@ -41,7 +43,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
*/ */
object NiaIcons { object NiaIcons {
val Add = Icons.Rounded.Add val Add = Icons.Rounded.Add
val ArrowBack = Icons.Rounded.ArrowBack val ArrowBack = Icons.AutoMirrored.Rounded.ArrowBack
val Bookmark = Icons.Rounded.Bookmark val Bookmark = Icons.Rounded.Bookmark
val BookmarkBorder = Icons.Rounded.BookmarkBorder val BookmarkBorder = Icons.Rounded.BookmarkBorder
val Bookmarks = Icons.Rounded.Bookmarks val Bookmarks = Icons.Rounded.Bookmarks
@ -53,7 +55,7 @@ object NiaIcons {
val Person = Icons.Rounded.Person val Person = Icons.Rounded.Person
val Search = Icons.Rounded.Search val Search = Icons.Rounded.Search
val Settings = Icons.Rounded.Settings val Settings = Icons.Rounded.Settings
val ShortText = Icons.Rounded.ShortText val ShortText = Icons.AutoMirrored.Rounded.ShortText
val Upcoming = Icons.Rounded.Upcoming val Upcoming = Icons.Rounded.Upcoming
val UpcomingBorder = Icons.Outlined.Upcoming val UpcomingBorder = Icons.Outlined.Upcoming
val ViewDay = Icons.Rounded.ViewDay val ViewDay = Icons.Rounded.ViewDay

@ -19,6 +19,9 @@ package com.google.samples.apps.nowinandroid.core.designsystem.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.text.style.LineHeightStyle.Trim
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
/** /**
@ -60,12 +63,20 @@ internal val NiaTypography = Typography(
fontSize = 24.sp, fontSize = 24.sp,
lineHeight = 32.sp, lineHeight = 32.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = Alignment.Bottom,
trim = Trim.None,
),
), ),
titleLarge = TextStyle( titleLarge = TextStyle(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 22.sp, fontSize = 22.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = Alignment.Bottom,
trim = Trim.LastLineBottom,
),
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@ -79,11 +90,16 @@ internal val NiaTypography = Typography(
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
), ),
// Default text style
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = Alignment.Center,
trim = Trim.None,
),
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
@ -97,22 +113,37 @@ internal val NiaTypography = Typography(
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.4.sp, letterSpacing = 0.4.sp,
), ),
// Used for Button
labelLarge = TextStyle( labelLarge = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
lineHeightStyle = LineHeightStyle(
alignment = Alignment.Center,
trim = Trim.LastLineBottom,
),
), ),
// Used for Navigation items
labelMedium = TextStyle( labelMedium = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = Alignment.Center,
trim = Trim.LastLineBottom,
),
), ),
// Used for Tag
labelSmall = TextStyle( labelSmall = TextStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 10.sp, fontSize = 10.sp,
lineHeight = 16.sp, lineHeight = 14.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = Alignment.Center,
trim = Trim.LastLineBottom,
),
), ),
) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 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.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 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: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.8 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: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 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: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 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.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.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: 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: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.0 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.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 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: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 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: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.core.network.di package com.google.samples.apps.nowinandroid.core.network.di
import android.content.Context import android.content.Context
import androidx.tracing.trace
import coil.ImageLoader import coil.ImageLoader
import coil.decode.SvgDecoder import coil.decode.SvgDecoder
import coil.util.DebugLogger import coil.util.DebugLogger
@ -51,16 +52,18 @@ internal object NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder() fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") {
.addInterceptor( OkHttpClient.Builder()
HttpLoggingInterceptor() .addInterceptor(
.apply { HttpLoggingInterceptor()
if (BuildConfig.DEBUG) { .apply {
setLevel(HttpLoggingInterceptor.Level.BODY) if (BuildConfig.DEBUG) {
} setLevel(HttpLoggingInterceptor.Level.BODY)
}, }
) },
.build() )
.build()
}
/** /**
* Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this * Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this
@ -72,20 +75,21 @@ internal object NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun imageLoader( fun imageLoader(
okHttpCallFactory: Call.Factory, // We specifically request dagger.Lazy here, so that it's not instantiated from Dagger.
okHttpCallFactory: dagger.Lazy<Call.Factory>,
@ApplicationContext application: Context, @ApplicationContext application: Context,
): ImageLoader = ImageLoader.Builder(application) ): ImageLoader = trace("NiaImageLoader") {
.callFactory(okHttpCallFactory) ImageLoader.Builder(application)
.components { .callFactory { okHttpCallFactory.get() }
add(SvgDecoder.Factory()) .components { add(SvgDecoder.Factory()) }
} // Assume most content images are versioned urls
// Assume most content images are versioned urls // but some problematic images are fetching each time
// but some problematic images are fetching each time .respectCacheHeaders(false)
.respectCacheHeaders(false) .apply {
.apply { if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG) { logger(DebugLogger())
logger(DebugLogger()) }
} }
} .build()
.build() }
} }

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.network.retrofit package com.google.samples.apps.nowinandroid.core.network.retrofit
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.network.BuildConfig import com.google.samples.apps.nowinandroid.core.network.BuildConfig
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
@ -73,17 +74,21 @@ private data class NetworkResponse<T>(
@Singleton @Singleton
internal class RetrofitNiaNetwork @Inject constructor( internal class RetrofitNiaNetwork @Inject constructor(
networkJson: Json, networkJson: Json,
okhttpCallFactory: Call.Factory, okhttpCallFactory: dagger.Lazy<Call.Factory>,
) : NiaNetworkDataSource { ) : NiaNetworkDataSource {
private val networkApi = Retrofit.Builder() private val networkApi = trace("RetrofitNiaNetwork") {
.baseUrl(NIA_BASE_URL) Retrofit.Builder()
.callFactory(okhttpCallFactory) .baseUrl(NIA_BASE_URL)
.addConverterFactory( // We use callFactory lambda here with dagger.Lazy<Call.Factory>
networkJson.asConverterFactory("application/json".toMediaType()), // to prevent initializing OkHttp on the main thread.
) .callFactory { okhttpCallFactory.get().newCall(it) }
.build() .addConverterFactory(
.create(RetrofitNiaNetworkApi::class.java) networkJson.asConverterFactory("application/json".toMediaType()),
)
.build()
.create(RetrofitNiaNetworkApi::class.java)
}
override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> = override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> =
networkApi.getTopics(ids = ids).data networkApi.getTopics(ids = ids).data

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

Loading…
Cancel
Save