Merge branch 'main' into mv/collectAsState

pull/166/head
Manuel Vivo 3 years ago
commit c5e09e4071

@ -117,7 +117,7 @@ jobs:
disable-animations: true disable-animations: true
disk-size: 1500M disk-size: 1500M
heap-size: 512M heap-size: 512M
script: ./gradlew connectedProdDebugAndroidTest -x :benchmark:connectedBenchmarkAndroidTest script: ./gradlew connectedProdDebugAndroidTest -x :benchmark:connectedProdBenchmarkAndroidTest --stacktrace
- name: Upload test reports - name: Upload test reports
if: always() if: always()

@ -24,6 +24,7 @@ plugins {
id("jacoco") id("jacoco")
id("dagger.hilt.android.plugin") id("dagger.hilt.android.plugin")
id("nowinandroid.spotless") id("nowinandroid.spotless")
id("nowinandroid.firebase-perf")
} }
android { android {
@ -62,8 +63,6 @@ android {
proguardFiles("benchmark-rules.pro") proguardFiles("benchmark-rules.pro")
// FIXME enabling minification breaks access to demo backend. // FIXME enabling minification breaks access to demo backend.
isMinifyEnabled = false isMinifyEnabled = false
// Keep the build type debuggable so we can attach a debugger if needed.
isDebuggable = true
applicationIdSuffix = ".benchmark" applicationIdSuffix = ".benchmark"
} }
} }

File diff suppressed because it is too large Load Diff

@ -22,12 +22,21 @@ import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.metrics.performance.JankStats
import com.google.samples.apps.nowinandroid.ui.NiaApp import com.google.samples.apps.nowinandroid.ui.NiaApp
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
/**
* Lazily inject [JankStats], which is used to track jank throughout the app.
*/
@Inject
lateinit var lazyStats: dagger.Lazy<JankStats>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -35,6 +44,18 @@ class MainActivity : ComponentActivity() {
// including IME animations // including IME animations
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { NiaApp(calculateWindowSizeClass(this)) } setContent {
NiaApp(calculateWindowSizeClass(this))
}
}
override fun onResume() {
super.onResume()
lazyStats.get().isTrackingEnabled = true
}
override fun onPause() {
super.onPause()
lazyStats.get().isTrackingEnabled = false
} }
} }

@ -0,0 +1,63 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.di
import android.app.Activity
import android.util.Log
import android.view.Window
import androidx.metrics.performance.JankStats
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import java.util.concurrent.Executor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
@Module
@InstallIn(ActivityComponent::class)
object JankStatsModule {
@Provides
fun providesOnFrameListener(): JankStats.OnFrameListener {
return JankStats.OnFrameListener { frameData ->
// Make sure to only log janky frames.
if (frameData.isJank) {
// We're currently logging this but would better report it to a backend.
Log.v("NiA Jank", frameData.toString())
}
}
}
@Provides
fun providesWindow(activity: Activity): Window {
return activity.window
}
@Provides
fun providesDefaultExecutor(): Executor {
return Dispatchers.Default.asExecutor()
}
@Provides
fun providesJankStats(
window: Window,
executor: Executor,
frameListener: JankStats.OnFrameListener
): JankStats {
return JankStats.createAndTrack(window, executor, frameListener)
}
}

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.navigation
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
@ -39,18 +40,20 @@ import com.google.samples.apps.nowinandroid.feature.interests.navigation.Interes
class NiaTopLevelNavigation(private val navController: NavHostController) { class NiaTopLevelNavigation(private val navController: NavHostController) {
fun navigateTo(destination: TopLevelDestination) { fun navigateTo(destination: TopLevelDestination) {
navController.navigate(destination.route) { trace("Navigation: $destination") {
// Pop up to the start destination of the graph to navController.navigate(destination.route) {
// avoid building up a large stack of destinations // Pop up to the start destination of the graph to
// on the back stack as users select items // avoid building up a large stack of destinations
popUpTo(navController.graph.findStartDestination().id) { // on the back stack as users select items
saveState = true popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
} }
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
} }
} }
} }

@ -39,10 +39,14 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
@ -55,12 +59,17 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavig
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
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.ui.JankMetricDisposableEffect
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation
import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_DESTINATIONS import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_DESTINATIONS
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @OptIn(
ExperimentalMaterial3Api::class,
ExperimentalLayoutApi::class,
ExperimentalComposeUiApi::class
)
@Composable @Composable
fun NiaApp(windowSizeClass: WindowSizeClass) { fun NiaApp(windowSizeClass: WindowSizeClass) {
NiaTheme { NiaTheme {
@ -74,7 +83,9 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
NiaBackground { NiaBackground {
Scaffold( Scaffold(
modifier = Modifier, modifier = Modifier.semantics {
testTagsAsResourceId = true
},
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
bottomBar = { bottomBar = {
@ -117,6 +128,17 @@ fun NiaApp(windowSizeClass: WindowSizeClass) {
} }
} }
} }
JankMetricDisposableEffect(navController) { metricsHolder ->
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
metricsHolder.state?.addState("Navigation", destination.route.toString())
}
navController.addOnDestinationChangedListener(listener)
onDispose {
navController.removeOnDestinationChangedListener(listener)
}
}
} }
} }

@ -13,6 +13,9 @@
* 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.Flavor
import com.google.samples.apps.nowinandroid.FlavorDimension
import com.google.samples.apps.nowinandroid.configureFlavors
plugins { plugins {
id("nowinandroid.android.test") id("nowinandroid.android.test")
@ -25,7 +28,6 @@ android {
defaultConfig { defaultConfig {
minSdk = 23 minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy("contentType", "demo")
} }
buildTypes { buildTypes {
@ -33,12 +35,18 @@ android {
// release build (for example, with minification on). It's signed with a debug key // release build (for example, with minification on). It's signed with a debug key
// for easy local/CI testing. // for easy local/CI testing.
val benchmark by creating { val benchmark by creating {
isDebuggable = false // Keep the build type debuggable so we can attach a debugger if needed.
isDebuggable = true
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release") matchingFallbacks.add("release")
} }
} }
// 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
// benchmarks on demo, so we benchmark on stable data.
configureFlavors(this)
targetProjectPath = ":app" targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true experimentalProperties["android.experimental.self-instrumenting"] = true
} }

@ -0,0 +1,25 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
import com.google.samples.apps.nowinandroid.benchmark.BuildConfig
/**
* Convenience parameter to use proper package name with regards to build type and build flavor.
*/
const val PACKAGE_NAME =
"com.google.samples.apps.nowinandroid.${BuildConfig.FLAVOR}.${BuildConfig.BUILD_TYPE}"

@ -19,7 +19,12 @@ package com.google.samples.apps.nowinandroid.baselineprofile
import androidx.benchmark.macro.ExperimentalBaselineProfilesApi import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
import androidx.benchmark.macro.junit4.BaselineProfileRule import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.foryou.forYouScrollFeedDownUp
import com.google.samples.apps.nowinandroid.foryou.forYouSelectAuthors
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
import com.google.samples.apps.nowinandroid.interests.interestsScrollPeopleDownUp
import com.google.samples.apps.nowinandroid.interests.interestsScrollTopicsDownUp
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -31,27 +36,30 @@ class BaselineProfileGenerator {
@get:Rule val baselineProfileRule = BaselineProfileRule() @get:Rule val baselineProfileRule = BaselineProfileRule()
@Test @Test
fun startup() = fun generate() =
baselineProfileRule.collectBaselineProfile( baselineProfileRule.collectBaselineProfile(PACKAGE_NAME) {
packageName = "com.google.samples.apps.nowinandroid.demo.benchmark"
) {
pressHome()
// This block defines the app's critical user journey. Here we are interested in // This block defines the app's critical user journey. Here we are interested in
// optimizing for app startup. But you can also navigate and scroll // optimizing for app startup. But you can also navigate and scroll
// through your most important UI. // through your most important UI.
pressHome()
startActivityAndWait() startActivityAndWait()
// Scroll the feed critical user journey
forYouWaitForContent()
forYouSelectAuthors()
forYouScrollFeedDownUp()
// Navigate to interests screen
device.findObject(By.text("Interests")).click()
device.waitForIdle()
interestsScrollTopicsDownUp()
// Navigate to people tab
device.findObject(By.text("People")).click()
device.waitForIdle() device.waitForIdle()
device.run { interestsScrollPeopleDownUp()
findObject(By.text("Interests"))
.click()
waitForIdle()
findObject(By.text("Accessibility")).scroll(Direction.DOWN, 2000f)
waitForIdle()
findObject(By.text("People")).click()
waitForIdle()
findObject(By.textStartsWith("Android")).scroll(Direction.DOWN, 2000f)
waitForIdle()
}
} }
} }

@ -0,0 +1,44 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.foryou
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
fun MacrobenchmarkScope.forYouWaitForContent() {
// Wait until content is loaded
device.wait(Until.hasObject(By.text("What are you interested in?")), 30_000)
}
fun MacrobenchmarkScope.forYouSelectAuthors() {
val authors = device.findObject(By.res("forYou:authors"))
// select some authors to show some feed content
repeat(3) { index ->
val author = authors.children[index % authors.childCount]
author.click()
device.waitForIdle()
}
}
fun MacrobenchmarkScope.forYouScrollFeedDownUp() {
val feedList = device.findObject(By.res("forYou:feed"))
feedList.fling(Direction.DOWN)
device.waitForIdle()
feedList.fling(Direction.UP)
}

@ -0,0 +1,56 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.foryou
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4ClassRunner::class)
class ScrollForYouFeedBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun scrollFeedCompilationNone() = scrollFeed(CompilationMode.None())
@Test
fun scrollFeedCompilationBaselineProfile() = scrollFeed(CompilationMode.Partial())
private fun scrollFeed(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric()),
compilationMode = compilationMode,
iterations = 10,
startupMode = StartupMode.COLD,
setupBlock = {
// Start the app
pressHome()
startActivityAndWait()
}
) {
forYouWaitForContent()
forYouSelectAuthors()
forYouScrollFeedDownUp()
}
}

@ -0,0 +1,35 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.interests
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
fun MacrobenchmarkScope.interestsScrollTopicsDownUp() {
val topicsList = device.findObject(By.res("interests:topics"))
topicsList.fling(Direction.DOWN)
device.waitForIdle()
topicsList.fling(Direction.UP)
}
fun MacrobenchmarkScope.interestsScrollPeopleDownUp() {
val peopleList = device.findObject(By.res("interests:people"))
peopleList.fling(Direction.DOWN)
device.waitForIdle()
peopleList.fling(Direction.UP)
}

@ -26,6 +26,8 @@ import androidx.benchmark.macro.StartupMode.WARM
import androidx.benchmark.macro.StartupTimingMetric import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -74,7 +76,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
fun startupFullCompilation() = startup(CompilationMode.Full()) fun startupFullCompilation() = startup(CompilationMode.Full())
private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
packageName = "com.google.samples.apps.nowinandroid", packageName = PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()), metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode, compilationMode = compilationMode,
iterations = 10, iterations = 10,
@ -84,5 +86,7 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
} }
) { ) {
startActivityAndWait() startActivityAndWait()
// Waits until the content is ready to capture Time To Full Display
forYouWaitForContent()
} }
} }

@ -69,5 +69,9 @@ gradlePlugin {
id = "nowinandroid.spotless" id = "nowinandroid.spotless"
implementationClass = "SpotlessConventionPlugin" implementationClass = "SpotlessConventionPlugin"
} }
register("firebase-perf") {
id = "nowinandroid.firebase-perf"
implementationClass = "FirebasePerfConventionPlugin"
}
} }
} }

@ -0,0 +1,29 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.gradle.api.Plugin
import org.gradle.api.Project
class FirebasePerfConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.findPlugin("com.google.firebase.firebase-perf").apply {
version = "1.4.1"
}
}
}
}

@ -36,7 +36,7 @@ internal fun Project.configureAndroidCompose(
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = libs.findVersion("androidxCompose").get().toString() kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString()
} }
kotlinOptions { kotlinOptions {

Binary file not shown.

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -70,3 +70,8 @@ dependencies {
kapt(libs.hilt.compiler) kapt(libs.hilt.compiler)
kaptAndroidTest(libs.hilt.compiler) kaptAndroidTest(libs.hilt.compiler)
} }
// TODO b/239411851, Remove kapt workaround configuration
kapt {
correctErrorTypes = true
}

@ -45,4 +45,6 @@ dependencies {
api(libs.androidx.compose.ui.util) api(libs.androidx.compose.ui.util)
api(libs.androidx.compose.runtime) api(libs.androidx.compose.runtime)
api(libs.androidx.compose.runtime.livedata) api(libs.androidx.compose.runtime.livedata)
api(libs.androidx.metrics)
api(libs.androidx.tracing.ktx)
} }

@ -0,0 +1,90 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.platform.LocalView
import androidx.metrics.performance.PerformanceMetricsState
import androidx.metrics.performance.PerformanceMetricsState.MetricsStateHolder
import kotlinx.coroutines.CoroutineScope
/**
* Retrieves [PerformanceMetricsState.MetricsStateHolder] from current [LocalView] and
* remembers it until the View changes.
* @see PerformanceMetricsState.getForHierarchy
*/
@Composable
fun rememberMetricsStateHolder(): MetricsStateHolder {
val localView = LocalView.current
return remember(localView) {
PerformanceMetricsState.getForHierarchy(localView)
}
}
/**
* Convenience function to work with [PerformanceMetricsState] state. The side effect is
* re-launched if any of the [keys] value is not equal to the previous composition.
* @see JankMetricDisposableEffect if you need to work with DisposableEffect to cleanup added state.
*/
@Composable
fun JankMetricEffect(
vararg keys: Any?,
reportMetric: suspend CoroutineScope.(state: MetricsStateHolder) -> Unit
) {
val metrics = rememberMetricsStateHolder()
LaunchedEffect(metrics, *keys) {
reportMetric(metrics)
}
}
/**
* Convenience function to work with [PerformanceMetricsState] state that needs to be cleaned up.
* The side effect is re-launched if any of the [keys] value is not equal to the previous composition.
*/
@Composable
fun JankMetricDisposableEffect(
vararg keys: Any?,
reportMetric: DisposableEffectScope.(state: MetricsStateHolder) -> DisposableEffectResult
) {
val metrics = rememberMetricsStateHolder()
DisposableEffect(metrics, *keys) {
reportMetric(this, metrics)
}
}
@Composable
fun TrackScrollJank(scrollableState: ScrollableState, stateName: String) {
JankMetricEffect(scrollableState) { metricsHolder ->
snapshotFlow { scrollableState.isScrollInProgress }.collect { isScrollInProgress ->
metricsHolder.state?.apply {
if (isScrollInProgress) {
addState(stateName, "Scrolling=true")
} else {
removeState(stateName)
}
}
}
}
}

@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
@ -43,6 +44,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@ -57,6 +59,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
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.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
@Composable @Composable
fun AuthorsCarousel( fun AuthorsCarousel(
@ -64,10 +67,15 @@ fun AuthorsCarousel(
onAuthorClick: (String, Boolean) -> Unit, onAuthorClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val lazyListState = rememberLazyListState()
val tag = "forYou:authors"
TrackScrollJank(scrollableState = lazyListState, stateName = tag)
LazyRow( LazyRow(
modifier = modifier, modifier = modifier.testTag(tag),
contentPadding = PaddingValues(24.dp), contentPadding = PaddingValues(24.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp) horizontalArrangement = Arrangement.spacedBy(24.dp),
state = lazyListState
) { ) {
items(items = authors, key = { item -> item.author.id }) { followableAuthor -> items(items = authors, key = { item -> item.author.id }) { followableAuthor ->
AuthorItem( AuthorItem(

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.foryou package com.google.samples.apps.nowinandroid.feature.foryou
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.annotation.IntRange import androidx.annotation.IntRange
@ -45,7 +46,9 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -59,12 +62,15 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz
import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -73,7 +79,9 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.trace
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnPreDraw
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -92,6 +100,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
import com.google.samples.apps.nowinandroid.core.ui.NewsResourceCardExpanded import com.google.samples.apps.nowinandroid.core.ui.NewsResourceCardExpanded
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import kotlin.math.floor import kotlin.math.floor
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@ -134,11 +143,11 @@ fun ForYouScreen(
titleRes = R.string.top_app_bar_title, titleRes = R.string.top_app_bar_title,
navigationIcon = NiaIcons.Search, navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource( navigationIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc id = R.string.for_you_top_app_bar_action_search
), ),
actionIcon = NiaIcons.AccountCircle, actionIcon = NiaIcons.AccountCircle,
actionIconContentDescription = stringResource( actionIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc id = R.string.for_you_top_app_bar_action_my_account
), ),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent containerColor = Color.Transparent
@ -163,8 +172,37 @@ fun ForYouScreen(
else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1) else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1)
} }
// Workaround to call Activity.reportFullyDrawn from Jetpack Compose.
// This code should be called when the UI is ready for use
// and relates to Time To Full Display.
val interestsLoaded =
interestsSelectionState !is ForYouInterestsSelectionUiState.Loading
val feedLoaded = feedState !is ForYouFeedUiState.Loading
if (interestsLoaded && feedLoaded) {
val localView = LocalView.current
// We use Unit to call reportFullyDrawn only on the first recomposition,
// however it will be called again if this composable goes out of scope.
// Activity.reportFullyDrawn() has its own check for this
// and is safe to call multiple times though.
LaunchedEffect(Unit) {
// We're leveraging the fact, that the current view is directly set as content of Activity.
val activity = localView.context as? Activity ?: return@LaunchedEffect
// To be sure not to call in the middle of a frame draw.
localView.doOnPreDraw { activity.reportFullyDrawn() }
}
}
val tag = "forYou:feed"
val lazyListState = rememberLazyListState()
TrackScrollJank(scrollableState = lazyListState, stateName = tag)
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.testTag(tag),
state = lazyListState,
) { ) {
InterestsSelection( InterestsSelection(
interestsSelectionState = interestsSelectionState, interestsSelectionState = interestsSelectionState,
@ -215,7 +253,8 @@ private fun LazyListScope.InterestsSelection(
NiaLoadingWheel( NiaLoadingWheel(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentSize(), .wrapContentSize()
.testTag("forYou:loading"),
contentDesc = stringResource(id = R.string.for_you_loading), contentDesc = stringResource(id = R.string.for_you_loading),
) )
} }
@ -287,8 +326,12 @@ private fun TopicSelection(
interestsSelectionState: ForYouInterestsSelectionUiState.WithInterestsSelection, interestsSelectionState: ForYouInterestsSelectionUiState.WithInterestsSelection,
onTopicCheckedChanged: (String, Boolean) -> Unit, onTopicCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) = trace("TopicSelection") {
val lazyGridState = rememberLazyGridState()
TrackScrollJank(scrollableState = lazyGridState, stateName = "forYou:TopicSelection")
LazyHorizontalGrid( LazyHorizontalGrid(
state = lazyGridState,
rows = GridCells.Fixed(3), rows = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
@ -326,7 +369,7 @@ private fun SingleTopicButton(
imageUrl: String, imageUrl: String,
isSelected: Boolean, isSelected: Boolean,
onClick: (String, Boolean) -> Unit onClick: (String, Boolean) -> Unit
) { ) = trace("SingleTopicButton") {
Surface( Surface(
modifier = Modifier modifier = Modifier
.width(312.dp) .width(312.dp)

@ -24,8 +24,8 @@
<string name="onboarding_guidance_title">What are you interested in?</string> <string name="onboarding_guidance_title">What are you interested in?</string>
<string name="onboarding_guidance_subtitle">Updates from topics you follow will appear here. Follow some things to get started.</string> <string name="onboarding_guidance_subtitle">Updates from topics you follow will appear here. Follow some things to get started.</string>
<string name="top_app_bar_title">Now in Android</string> <string name="top_app_bar_title">Now in Android</string>
<string name="top_app_bar_action_button_content_desc">My account</string> <string name="for_you_top_app_bar_action_my_account">My account</string>
<string name="top_app_bar_navigation_button_content_desc">Search</string> <string name="for_you_top_app_bar_action_search">Search</string>
<!-- Authors--> <!-- Authors-->
<string name="following">You are following</string> <string name="following">You are following</string>

@ -44,6 +44,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
@ -66,6 +67,14 @@ fun InterestsRoute(
switchTab = viewModel::switchTab, switchTab = viewModel::switchTab,
modifier = modifier modifier = modifier
) )
JankMetricDisposableEffect(tabState) { metricsHolder ->
metricsHolder.state?.addState("Interests:TabState", "currentIndex:${tabState.currentIndex}")
onDispose {
metricsHolder.state?.removeState("Interests:TabState")
}
}
} }
@Composable @Composable
@ -89,11 +98,11 @@ fun InterestsScreen(
titleRes = R.string.interests, titleRes = R.string.interests,
navigationIcon = NiaIcons.Search, navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource( navigationIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc id = R.string.interests_top_app_bar_action_seearch
), ),
actionIcon = NiaIcons.MoreVert, actionIcon = NiaIcons.MoreVert,
actionIconContentDescription = stringResource( actionIconContentDescription = stringResource(
id = R.string.top_app_bar_navigation_button_content_desc id = R.string.interests_top_app_bar_action_menu
), ),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent containerColor = Color.Transparent

@ -27,6 +27,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
@ -39,7 +40,9 @@ fun TopicsTabContent(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn( LazyColumn(
modifier = modifier.padding(horizontal = 16.dp), modifier = modifier
.padding(horizontal = 16.dp)
.testTag("interests:topics"),
contentPadding = PaddingValues(top = 8.dp) contentPadding = PaddingValues(top = 8.dp)
) { ) {
topics.forEach { followableTopic -> topics.forEach { followableTopic ->
@ -69,7 +72,9 @@ fun AuthorsTabContent(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn( LazyColumn(
modifier = modifier.padding(horizontal = 16.dp), modifier = modifier
.padding(horizontal = 16.dp)
.testTag("interests:people"),
contentPadding = PaddingValues(top = 8.dp) contentPadding = PaddingValues(top = 8.dp)
) { ) {
authors.forEach { followableAuthor -> authors.forEach { followableAuthor ->

@ -22,6 +22,6 @@
<string name="interests_empty_header">"No available data"</string> <string name="interests_empty_header">"No available data"</string>
<string name="interests_card_follow_button_content_desc">Follow interest button</string> <string name="interests_card_follow_button_content_desc">Follow interest button</string>
<string name="interests_card_unfollow_button_content_desc">Unfollow interest button</string> <string name="interests_card_unfollow_button_content_desc">Unfollow interest button</string>
<string name="top_app_bar_action_button_content_desc">Menu</string> <string name="interests_top_app_bar_action_menu">Menu</string>
<string name="top_app_bar_navigation_button_content_desc">Search</string> <string name="interests_top_app_bar_action_seearch">Search</string>
</resources> </resources>

@ -17,9 +17,13 @@
package com.google.samples.apps.nowinandroid.feature.topic package com.google.samples.apps.nowinandroid.feature.topic
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.hasScrollToNodeAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToNode
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
@ -115,10 +119,11 @@ class TopicScreenTest {
) )
} }
// First news title shown // Scroll to first news title if available
composeTestRule composeTestRule
.onNodeWithText(sampleNewsResources.first().title) .onAllNodes(hasScrollToNodeAction())
.assertExists() .onFirst()
.performScrollToNode(hasText(sampleNewsResources.first().title))
} }
} }

@ -5,6 +5,7 @@ androidGradlePlugin = "7.2.1"
androidxActivity = "1.4.0" androidxActivity = "1.4.0"
androidxAppCompat = "1.4.2" androidxAppCompat = "1.4.2"
androidxCompose = "1.2.0-rc02" androidxCompose = "1.2.0-rc02"
androidxComposeCompiler = "1.2.0"
androidxComposeMaterial3 = "1.0.0-alpha13" androidxComposeMaterial3 = "1.0.0-alpha13"
androidxCore = "1.8.0" androidxCore = "1.8.0"
androidxCustomView = "1.0.0-rc01" androidxCustomView = "1.0.0-rc01"
@ -13,13 +14,15 @@ androidxEspresso = "3.4.0"
androidxHiltNavigationCompose = "1.0.0" androidxHiltNavigationCompose = "1.0.0"
androidxLifecycle = "2.6.0-alpha01" androidxLifecycle = "2.6.0-alpha01"
androidxMacroBenchmark = "1.1.0" androidxMacroBenchmark = "1.1.0"
androidxNavigation = "2.4.2" androidxNavigation = "2.5.0"
androidxMetrics = "1.0.0-alpha01"
androidxProfileinstaller = "1.2.0-rc01" androidxProfileinstaller = "1.2.0-rc01"
androidxSavedState = "1.1.0" androidxSavedState = "1.1.0"
androidxStartup = "1.1.1" androidxStartup = "1.1.1"
androidxWindowManager = "1.0.0" androidxWindowManager = "1.0.0"
androidxTest = "1.4.0" androidxTest = "1.4.0"
androidxTestExt = "1.1.3" androidxTestExt = "1.1.3"
androidxTracing = "1.1.0"
androidxUiAutomator = "2.2.0" androidxUiAutomator = "2.2.0"
androidxWork = "2.7.1" androidxWork = "2.7.1"
coil = "2.1.0" coil = "2.1.0"
@ -27,17 +30,17 @@ hilt = "2.42"
hiltExt = "1.0.0" hiltExt = "1.0.0"
jacoco = "0.8.7" jacoco = "0.8.7"
junit4 = "4.13.2" junit4 = "4.13.2"
kotlin = "1.6.21" kotlin = "1.7.0"
kotlinxCoroutines = "1.6.3" kotlinxCoroutines = "1.6.3"
kotlinxDatetime = "0.3.3" kotlinxDatetime = "0.3.3"
kotlinxSerializationJson = "1.3.3" kotlinxSerializationJson = "1.3.3"
ksp = "1.6.21-1.0.5" ksp = "1.7.0-1.0.6"
ktlint = "0.43.0" ktlint = "0.43.0"
lint = "30.2.1" lint = "30.2.1"
material3 = "1.6.1" material3 = "1.6.1"
okhttp = "4.10.0" okhttp = "4.10.0"
protobuf = "3.21.1" protobuf = "3.21.1"
protobufPlugin = "0.8.18" protobufPlugin = "0.8.19"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "0.8.0" retrofitKotlinxSerializationJson = "0.8.0"
room = "2.4.2" room = "2.4.2"
@ -71,6 +74,7 @@ androidx-dataStore-preferences = { group = "androidx.datastore", name = "datasto
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" }
androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref= "androidxSavedState"} androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref= "androidxSavedState"}
@ -82,6 +86,7 @@ androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espres
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" } androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" }
androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" } androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" }
androidx-tracing-ktx = {group = "androidx.tracing", name="tracing-ktx", version.ref = "androidxTracing" }
androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" } androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" }
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil"} coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil"}

@ -35,6 +35,7 @@ dependencies {
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.startup) implementation(libs.androidx.startup)
implementation(libs.androidx.work.ktx) implementation(libs.androidx.work.ktx)
implementation(libs.hilt.ext.work) implementation(libs.hilt.ext.work)

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.sync.workers
import android.content.Context import android.content.Context
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.tracing.traceAsync
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
@ -59,15 +60,17 @@ class SyncWorker @AssistedInject constructor(
appContext.syncForegroundInfo() appContext.syncForegroundInfo()
override suspend fun doWork(): Result = withContext(ioDispatcher) { override suspend fun doWork(): Result = withContext(ioDispatcher) {
// First sync the repositories in parallel traceAsync("Sync", 0) {
val syncedSuccessfully = awaitAll( // First sync the repositories in parallel
async { topicRepository.sync() }, val syncedSuccessfully = awaitAll(
async { authorsRepository.sync() }, async { topicRepository.sync() },
async { newsRepository.sync() }, async { authorsRepository.sync() },
).all { it } async { newsRepository.sync() },
).all { it }
if (syncedSuccessfully) Result.success() if (syncedSuccessfully) Result.success()
else Result.retry() else Result.retry()
}
} }
override suspend fun getChangeListVersions(): ChangeListVersions = override suspend fun getChangeListVersions(): ChangeListVersions =

Loading…
Cancel
Save