From 103398682a37a595854f66ee79615ee124f548f4 Mon Sep 17 00:00:00 2001 From: Ben Weiss Date: Fri, 20 May 2022 12:08:47 +0100 Subject: [PATCH 01/13] Add tracing library and start using it Currently tracing sync and main navigation. --- .../navigation/NiaTopLevelNavigation.kt | 25 ++++++------ core-navigation/build.gradle.kts | 1 + .../core/navigation/NavigationExtensions.kt | 39 +++++++++++++++++++ core-ui/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + sync/build.gradle.kts | 1 + .../nowinandroid/sync/workers/SyncWorker.kt | 19 +++++---- 7 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt index f2404e88c..e76918eae 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/navigation/NiaTopLevelNavigation.kt @@ -24,6 +24,7 @@ import androidx.compose.material.icons.outlined.Upcoming import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController +import androidx.tracing.trace import com.google.samples.apps.nowinandroid.feature.foryou.R.string.for_you import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination import com.google.samples.apps.nowinandroid.feature.interests.R.string.interests @@ -41,18 +42,20 @@ import com.google.samples.apps.nowinandroid.feature.interests.navigation.Interes class NiaTopLevelNavigation(private val navController: NavHostController) { fun navigateTo(destination: TopLevelDestination) { - navController.navigate(destination.route) { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = true + trace("Navigation: $destination") { + navController.navigate(destination.route) { + // Pop up to the start destination of the graph to + // avoid building up a large stack of destinations + // on the back stack as users select items + 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 } } } diff --git a/core-navigation/build.gradle.kts b/core-navigation/build.gradle.kts index d9449babc..9e9ab81ae 100644 --- a/core-navigation/build.gradle.kts +++ b/core-navigation/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { api(libs.androidx.hilt.navigation.compose) api(libs.androidx.navigation.compose) + implementation(libs.androidx.tracing.ktx) implementation(libs.hilt.android) kapt(libs.hilt.compiler) } \ No newline at end of file diff --git a/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt b/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt new file mode 100644 index 000000000..cda2341a7 --- /dev/null +++ b/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt @@ -0,0 +1,39 @@ +/* + * 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.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDeepLink +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.tracing.trace + +/** + * Wrapping a [NavGraphBuilder.composable] in a trace block. + */ +fun NavGraphBuilder.tracedComposable( + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + content: @Composable (NavBackStackEntry) -> Unit +) { + trace("Navigation: $route") { + composable(route, arguments, deepLinks, content) + } +} diff --git a/core-ui/build.gradle.kts b/core-ui/build.gradle.kts index 7fade59be..b25eeb9eb 100644 --- a/core-ui/build.gradle.kts +++ b/core-ui/build.gradle.kts @@ -43,4 +43,5 @@ dependencies { api(libs.androidx.compose.ui.util) api(libs.androidx.compose.runtime) api(libs.androidx.compose.runtime.livedata) + api(libs.androidx.tracing.ktx) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 101950284..c4ebe2131 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ androidxStartup = "1.1.1" androidxWindowManager = "1.0.0" androidxTest = "1.4.0" androidxTestExt = "1.1.2" +androidxTracing = "1.1.0" androidxUiAutomator = "2.2.0" androidxWork = "2.7.1" coil = "2.0.0-rc01" @@ -81,6 +82,7 @@ androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espres androidx-test-runner = { group = "androidx.test", name = "runner", 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-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-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" } coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil"} diff --git a/sync/build.gradle.kts b/sync/build.gradle.kts index 531032a9f..56db29239 100644 --- a/sync/build.gradle.kts +++ b/sync/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.startup) implementation(libs.androidx.work.ktx) implementation(libs.hilt.ext.work) diff --git a/sync/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt b/sync/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt index 3f03e0b17..4ab9f9cda 100644 --- a/sync/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt +++ b/sync/src/main/java/com/google/samples/apps/nowinandroid/sync/workers/SyncWorker.kt @@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.sync.workers import android.content.Context import androidx.hilt.work.HiltWorker +import androidx.tracing.traceAsync import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.OneTimeWorkRequestBuilder @@ -59,15 +60,17 @@ class SyncWorker @AssistedInject constructor( appContext.syncForegroundInfo() override suspend fun doWork(): Result = withContext(ioDispatcher) { - // First sync the repositories in parallel - val syncedSuccessfully = awaitAll( - async { topicRepository.sync() }, - async { authorsRepository.sync() }, - async { newsRepository.sync() }, - ).all { it } + traceAsync("Sync", 0) { + // First sync the repositories in parallel + val syncedSuccessfully = awaitAll( + async { topicRepository.sync() }, + async { authorsRepository.sync() }, + async { newsRepository.sync() }, + ).all { it } - if (syncedSuccessfully) Result.success() - else Result.retry() + if (syncedSuccessfully) Result.success() + else Result.retry() + } } override suspend fun getChangeListVersions(): ChangeListVersions = From 4f5797565770d4b2b7241e4f87fb340e7bebf8ff Mon Sep 17 00:00:00 2001 From: Ben Weiss Date: Fri, 20 May 2022 16:24:32 +0100 Subject: [PATCH 02/13] Add jankStats and rudamentary jank logging --- .../samples/apps/nowinandroid/MainActivity.kt | 36 ++++++++++++++++- .../samples/apps/nowinandroid/ui/NiaApp.kt | 6 +++ .../core/navigation/NavigationExtensions.kt | 39 ------------------- core-ui/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + 5 files changed, 44 insertions(+), 40 deletions(-) delete mode 100644 core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt index c4dea1f2b..28983303e 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -17,17 +17,33 @@ package com.google.samples.apps.nowinandroid import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.remember import androidx.core.view.WindowCompat +import androidx.metrics.performance.JankStats +import androidx.metrics.performance.PerformanceMetricsState import com.google.samples.apps.nowinandroid.ui.NiaApp import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @AndroidEntryPoint class MainActivity : ComponentActivity() { + + private lateinit var jankStats: JankStats + + private val jankFrameListener = JankStats.OnFrameListener { frameData -> + // Make sure to only log janky frames. + if (frameData.isJank) { + Log.v("NiA Jank", frameData.toString()) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -35,6 +51,24 @@ class MainActivity : ComponentActivity() { // including IME animations WindowCompat.setDecorFitsSystemWindows(window, false) - setContent { NiaApp(calculateWindowSizeClass(this)) } + setContent { + NiaApp(calculateWindowSizeClass(this)) + } + + jankStats = JankStats.createAndTrack( + window, + Dispatchers.Default.asExecutor(), + jankFrameListener + ) + } + + override fun onResume() { + super.onResume() + jankStats.isTrackingEnabled = true + } + + override fun onPause() { + super.onPause() + jankStats.isTrackingEnabled = false } } diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 3a3e25b66..3b7feb9db 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -46,8 +46,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.metrics.performance.PerformanceMetricsState import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.compose.currentBackStackEntryAsState @@ -64,7 +66,11 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination @Composable fun NiaApp(windowSizeClass: WindowSizeClass) { NiaTheme { + val metricsStateHolder = PerformanceMetricsState.getForHierarchy(LocalView.current) val navController = rememberNavController() + navController.addOnDestinationChangedListener { _, destination, _ -> + metricsStateHolder.state?.addState("Navigation", "Jank at ${destination.route}") + } val niaTopLevelNavigation = remember(navController) { NiaTopLevelNavigation(navController) } diff --git a/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt b/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt deleted file mode 100644 index cda2341a7..000000000 --- a/core-navigation/src/main/java/com/google/samples/apps/nowinandroid/core/navigation/NavigationExtensions.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.core.navigation - -import androidx.compose.runtime.Composable -import androidx.navigation.NamedNavArgument -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavDeepLink -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.tracing.trace - -/** - * Wrapping a [NavGraphBuilder.composable] in a trace block. - */ -fun NavGraphBuilder.tracedComposable( - route: String, - arguments: List = emptyList(), - deepLinks: List = emptyList(), - content: @Composable (NavBackStackEntry) -> Unit -) { - trace("Navigation: $route") { - composable(route, arguments, deepLinks, content) - } -} diff --git a/core-ui/build.gradle.kts b/core-ui/build.gradle.kts index b25eeb9eb..3d47cb9ee 100644 --- a/core-ui/build.gradle.kts +++ b/core-ui/build.gradle.kts @@ -43,5 +43,6 @@ dependencies { api(libs.androidx.compose.ui.util) api(libs.androidx.compose.runtime) api(libs.androidx.compose.runtime.livedata) + api(libs.androidx.metrics) api(libs.androidx.tracing.ktx) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4ebe2131..5949dba81 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ androidxEspresso = "3.3.0" androidxHiltNavigationCompose = "1.0.0" androidxLifecycle = "2.5.0-beta01" androidxMacroBenchmark = "1.1.0-rc02" +androidxMetrics = "1.0.0-alpha01" androidxNavigation = "2.4.0-rc01" androidxProfileinstaller = "1.2.0-beta01" androidxSavedState = "1.1.0" @@ -71,6 +72,7 @@ androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", ve androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } 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-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref= "androidxSavedState"} From 895ec4c3372e995be68db388318b232f32b7db72 Mon Sep 17 00:00:00 2001 From: Ben Weiss Date: Fri, 20 May 2022 20:38:28 +0100 Subject: [PATCH 03/13] Inject JankStats with Hilt --- .../samples/apps/nowinandroid/MainActivity.kt | 31 ++++----- .../apps/nowinandroid/di/JankStatsModule.kt | 63 +++++++++++++++++++ .../samples/apps/nowinandroid/ui/NiaApp.kt | 2 +- 3 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt index 28983303e..de63dad5d 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -17,32 +17,25 @@ package com.google.samples.apps.nowinandroid import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.remember import androidx.core.view.WindowCompat import androidx.metrics.performance.JankStats -import androidx.metrics.performance.PerformanceMetricsState import com.google.samples.apps.nowinandroid.ui.NiaApp import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asExecutor +import javax.inject.Inject @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @AndroidEntryPoint class MainActivity : ComponentActivity() { - private lateinit var jankStats: JankStats + @Inject + lateinit var lazyStats: dagger.Lazy - private val jankFrameListener = JankStats.OnFrameListener { frameData -> - // Make sure to only log janky frames. - if (frameData.isJank) { - Log.v("NiA Jank", frameData.toString()) - } - } + @Inject + lateinit var jankFrameListener: JankStats.OnFrameListener override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -54,21 +47,19 @@ class MainActivity : ComponentActivity() { setContent { NiaApp(calculateWindowSizeClass(this)) } - - jankStats = JankStats.createAndTrack( - window, - Dispatchers.Default.asExecutor(), - jankFrameListener - ) } override fun onResume() { super.onResume() - jankStats.isTrackingEnabled = true + isTrackingEnabled(true) } override fun onPause() { super.onPause() - jankStats.isTrackingEnabled = false + isTrackingEnabled(false) + } + + private fun isTrackingEnabled(enabled: Boolean) { + lazyStats.get().isTrackingEnabled = enabled } } diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt new file mode 100644 index 000000000..6d11a9f68 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt @@ -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) + } +} diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 3b7feb9db..d1b647a8b 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -69,7 +69,7 @@ fun NiaApp(windowSizeClass: WindowSizeClass) { val metricsStateHolder = PerformanceMetricsState.getForHierarchy(LocalView.current) val navController = rememberNavController() navController.addOnDestinationChangedListener { _, destination, _ -> - metricsStateHolder.state?.addState("Navigation", "Jank at ${destination.route}") + metricsStateHolder.state?.addState("Navigation", "${destination.route}") } val niaTopLevelNavigation = remember(navController) { NiaTopLevelNavigation(navController) From a37c9328981dac6edcd146ab05441695cff9d113 Mon Sep 17 00:00:00 2001 From: Ben Weiss Date: Fri, 20 May 2022 21:18:25 +0100 Subject: [PATCH 04/13] Introduce view extension to track jank --- .../samples/apps/nowinandroid/ui/NiaApp.kt | 6 ++--- .../core/ui/JankStatsExtensions.kt | 24 +++++++++++++++++++ .../feature/interests/InterestsItem.kt | 3 +++ .../feature/interests/InterestsScreen.kt | 4 +++- 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index d1b647a8b..991f09994 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -49,12 +49,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.metrics.performance.PerformanceMetricsState import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.samples.apps.nowinandroid.core.ui.ClearRippleTheme +import com.google.samples.apps.nowinandroid.core.ui.addPerformanceMetricsState import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.navigation.NiaNavHost @@ -66,10 +66,10 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination @Composable fun NiaApp(windowSizeClass: WindowSizeClass) { NiaTheme { - val metricsStateHolder = PerformanceMetricsState.getForHierarchy(LocalView.current) + val localView = LocalView.current val navController = rememberNavController() navController.addOnDestinationChangedListener { _, destination, _ -> - metricsStateHolder.state?.addState("Navigation", "${destination.route}") + localView.addPerformanceMetricsState("Navigation", "${destination.route}") } val niaTopLevelNavigation = remember(navController) { NiaTopLevelNavigation(navController) diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt new file mode 100644 index 000000000..f6064ec74 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt @@ -0,0 +1,24 @@ +/* + * 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 android.view.View +import androidx.metrics.performance.PerformanceMetricsState + +fun View.addPerformanceMetricsState(stateName: String, state: String) { + PerformanceMetricsState.getForHierarchy(this).state?.addState(stateName, state) +} diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt index b9c2afadd..fc4f7d9ac 100644 --- a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt @@ -33,12 +33,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.google.samples.apps.nowinandroid.core.ui.FollowButton +import com.google.samples.apps.nowinandroid.core.ui.addPerformanceMetricsState import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.feature.interests.R.string @@ -54,6 +56,7 @@ fun InterestsItem( description: String = "", itemSeparation: Dp = 16.dp ) { + LocalView.current.addPerformanceMetricsState("InterestItem", name) Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index 7be087ccb..54fe3c9cf 100644 --- a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -33,10 +33,12 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel +import com.google.samples.apps.nowinandroid.core.ui.addPerformanceMetricsState import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar @@ -50,7 +52,6 @@ fun InterestsRoute( ) { val uiState by viewModel.uiState.collectAsState() val tabState by viewModel.tabState.collectAsState() - InterestsScreen( uiState = uiState, tabState = tabState, @@ -61,6 +62,7 @@ fun InterestsRoute( switchTab = viewModel::switchTab, modifier = modifier ) + LocalView.current.addPerformanceMetricsState("Interests", "$tabState") } @Composable From ccb08a1a06500ba9984c1f6abc588b2fd23074e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:31:44 +0200 Subject: [PATCH 05/13] Add rememberMetricsStateHolder composable Change-Id: I420cfaf826fee770975164147c29280a760bb5ff --- .../nowinandroid/core/ui/JankStatsExtensions.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt index f6064ec74..f1e7544af 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt @@ -17,8 +17,25 @@ package com.google.samples.apps.nowinandroid.core.ui import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView import androidx.metrics.performance.PerformanceMetricsState fun View.addPerformanceMetricsState(stateName: String, state: String) { PerformanceMetricsState.getForHierarchy(this).state?.addState(stateName, state) } + +/** + * Retrieves [PerformanceMetricsState.MetricsStateHolder] from current [LocalView] and + * remembers it until the View changes. + * @see PerformanceMetricsState.getForHierarchy + */ +@Composable +fun rememberMetricsStateHolder(): PerformanceMetricsState.MetricsStateHolder { + val localView = LocalView.current + + return remember(localView) { + PerformanceMetricsState.getForHierarchy(localView) + } +} From a076a500107795c559d9f63e94ea8d621c496d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:35:03 +0200 Subject: [PATCH 06/13] Use rememberMetricsStateHolder for navigation Change-Id: I3b038a9bbc6eed9b21343aa0fd75c044049386a4 --- .../samples/apps/nowinandroid/ui/NiaApp.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 991f09994..b495056fc 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -42,20 +42,21 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.samples.apps.nowinandroid.core.ui.ClearRippleTheme -import com.google.samples.apps.nowinandroid.core.ui.addPerformanceMetricsState import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground +import com.google.samples.apps.nowinandroid.core.ui.rememberMetricsStateHolder import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation @@ -66,11 +67,20 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination @Composable fun NiaApp(windowSizeClass: WindowSizeClass) { NiaTheme { - val localView = LocalView.current val navController = rememberNavController() - navController.addOnDestinationChangedListener { _, destination, _ -> - localView.addPerformanceMetricsState("Navigation", "${destination.route}") + val metricsHolder = rememberMetricsStateHolder() + DisposableEffect(navController, metricsHolder) { + val listener = NavController.OnDestinationChangedListener { _, destination, _ -> + metricsHolder.state?.addState("Navigation", destination.route.toString()) + } + + navController.addOnDestinationChangedListener(listener) + + onDispose { + navController.removeOnDestinationChangedListener(listener) + } } + val niaTopLevelNavigation = remember(navController) { NiaTopLevelNavigation(navController) } From 1ac3b4d90ca2181a5d26e3d300635bec1d7e96e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:37:30 +0200 Subject: [PATCH 07/13] Use DisposableEffect + rememberMetricsStateHolder for Interests tab selection Change-Id: I1b7820ebd4d9e16f0aa4d576c250e5aa137706c0 --- .../feature/interests/InterestsScreen.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index 54fe3c9cf..eeba8e53d 100644 --- a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -29,19 +29,19 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel -import com.google.samples.apps.nowinandroid.core.ui.addPerformanceMetricsState import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar +import com.google.samples.apps.nowinandroid.core.ui.rememberMetricsStateHolder @Composable fun InterestsRoute( @@ -62,7 +62,15 @@ fun InterestsRoute( switchTab = viewModel::switchTab, modifier = modifier ) - LocalView.current.addPerformanceMetricsState("Interests", "$tabState") + + val metricsHolder = rememberMetricsStateHolder() + DisposableEffect(tabState, metricsHolder) { + metricsHolder.state?.addState("Interests:TabState", "currentIndex:${tabState.currentIndex}") + + onDispose { + metricsHolder.state?.removeState("Interests:TabState") + } + } } @Composable From 24a876d4d8d207570eccad9742f2d672a7f63a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:38:27 +0200 Subject: [PATCH 08/13] Remove InterestItem state Change-Id: I5d4fddb9fb36a40ae9864edfa6eb4850d8f8d306 --- .../samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt | 5 ----- .../apps/nowinandroid/feature/interests/InterestsItem.kt | 3 --- 2 files changed, 8 deletions(-) diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt index f1e7544af..9bc9e9eeb 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt @@ -16,16 +16,11 @@ package com.google.samples.apps.nowinandroid.core.ui -import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalView import androidx.metrics.performance.PerformanceMetricsState -fun View.addPerformanceMetricsState(stateName: String, state: String) { - PerformanceMetricsState.getForHierarchy(this).state?.addState(stateName, state) -} - /** * Retrieves [PerformanceMetricsState.MetricsStateHolder] from current [LocalView] and * remembers it until the View changes. diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt index fc4f7d9ac..b9c2afadd 100644 --- a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt @@ -33,14 +33,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.google.samples.apps.nowinandroid.core.ui.FollowButton -import com.google.samples.apps.nowinandroid.core.ui.addPerformanceMetricsState import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.feature.interests.R.string @@ -56,7 +54,6 @@ fun InterestsItem( description: String = "", itemSeparation: Dp = 16.dp ) { - LocalView.current.addPerformanceMetricsState("InterestItem", name) Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier From dc97d15a8cc3060b935d18bc3b84c9e5b66b715f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:40:03 +0200 Subject: [PATCH 09/13] Add JankMetricEffect Change-Id: I8a5d90188b52ec1c11eec1c2724dda1332615768 --- .../core/ui/JankStatsExtensions.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt index 9bc9e9eeb..3d606b6a3 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt @@ -17,9 +17,11 @@ package com.google.samples.apps.nowinandroid.core.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalView import androidx.metrics.performance.PerformanceMetricsState +import kotlinx.coroutines.CoroutineScope /** * Retrieves [PerformanceMetricsState.MetricsStateHolder] from current [LocalView] and @@ -34,3 +36,19 @@ fun rememberMetricsStateHolder(): PerformanceMetricsState.MetricsStateHolder { 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: PerformanceMetricsState.MetricsStateHolder) -> Unit +) { + val metrics = rememberMetricsStateHolder() + LaunchedEffect(metrics, *keys) { + reportMetric(metrics) + } +} From 57a7b5dbe763836ff83e3a8df7e4e52503a643a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:40:08 +0200 Subject: [PATCH 10/13] Add AuthorsCarousel scrolling state Change-Id: I4ee60188fffdad4678f0f5a0ee4ef47031a1d61f --- .../feature/foryou/AuthorsCarousel.kt | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt index df6b8bbda..81024276d 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt @@ -26,12 +26,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -48,6 +50,9 @@ import coil.compose.AsyncImage 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.ui.FollowButton +import com.google.samples.apps.nowinandroid.core.ui.JankMetricEffect +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine @Composable fun AuthorsCarousel( @@ -55,7 +60,25 @@ fun AuthorsCarousel( onAuthorClick: (String, Boolean) -> Unit, modifier: Modifier = Modifier ) { - LazyRow(modifier) { + val lazyListState = rememberLazyListState() + + JankMetricEffect(lazyListState) { metricsHolder -> + combine( + snapshotFlow { lazyListState.isScrollInProgress }, + snapshotFlow { lazyListState.firstVisibleItemIndex } + ) { isScrollInProgress, firstVisibleItemIndex -> + if (isScrollInProgress) { + metricsHolder.state?.addState( + "ForYou:AuthorsCarousel:Scrolling", + "Index=${firstVisibleItemIndex}" + ) + } else { + metricsHolder.state?.removeState("ForYou:AuthorsCarousel:Scrolling") + } + }.collect() + } + + LazyRow(modifier, lazyListState) { items(items = authors, key = { item -> item.author.id }) { followableAuthor -> AuthorItem( author = followableAuthor.author, From bc7270d6c41e1b66fc62ef96496660ccf3c923df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:41:19 +0200 Subject: [PATCH 11/13] Add JankMetricDisposableEffect Change-Id: Idcab0faa2e9b4cc9986064f45217cab101843697 --- .../samples/apps/nowinandroid/ui/NiaApp.kt | 6 ++---- .../core/ui/JankStatsExtensions.kt | 19 +++++++++++++++++++ .../feature/interests/InterestsScreen.kt | 6 ++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index b495056fc..50b5f57da 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -42,7 +42,6 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -55,8 +54,8 @@ import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.samples.apps.nowinandroid.core.ui.ClearRippleTheme +import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground -import com.google.samples.apps.nowinandroid.core.ui.rememberMetricsStateHolder import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.NiaTopLevelNavigation @@ -68,8 +67,7 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination fun NiaApp(windowSizeClass: WindowSizeClass) { NiaTheme { val navController = rememberNavController() - val metricsHolder = rememberMetricsStateHolder() - DisposableEffect(navController, metricsHolder) { + JankMetricDisposableEffect(navController) { metricsHolder -> val listener = NavController.OnDestinationChangedListener { _, destination, _ -> metricsHolder.state?.addState("Navigation", destination.route.toString()) } diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt index 3d606b6a3..4b1459a2d 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt @@ -17,6 +17,9 @@ package com.google.samples.apps.nowinandroid.core.ui 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.ui.platform.LocalView @@ -52,3 +55,19 @@ fun JankMetricEffect( 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: PerformanceMetricsState.MetricsStateHolder) -> DisposableEffectResult +) { + val metrics = rememberMetricsStateHolder() + DisposableEffect(metrics, *keys) { + reportMetric(this, metrics) + } +} + diff --git a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index eeba8e53d..3a1c3ac7b 100644 --- a/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature-interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -37,11 +36,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar -import com.google.samples.apps.nowinandroid.core.ui.rememberMetricsStateHolder @Composable fun InterestsRoute( @@ -63,8 +62,7 @@ fun InterestsRoute( modifier = modifier ) - val metricsHolder = rememberMetricsStateHolder() - DisposableEffect(tabState, metricsHolder) { + JankMetricDisposableEffect(tabState) { metricsHolder -> metricsHolder.state?.addState("Interests:TabState", "currentIndex:${tabState.currentIndex}") onDispose { From 4dc1d0cd2ec979a6d72c64a3005e198af070f2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:51:48 +0200 Subject: [PATCH 12/13] Add ForYou feed scrolling state Change-Id: Ia371c564e4ff723fbeaca741d278174bd6db0dd8 --- .../feature/foryou/ForYouScreen.kt | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 2ab82c054..b29a3b6e0 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -63,6 +64,7 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -86,6 +88,7 @@ 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.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.ui.JankMetricEffect import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel import com.google.samples.apps.nowinandroid.core.ui.NewsResourceCardExpanded import com.google.samples.apps.nowinandroid.core.ui.component.NiaFilledButton @@ -95,8 +98,10 @@ import com.google.samples.apps.nowinandroid.core.ui.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography -import kotlin.math.floor +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.datetime.Instant +import kotlin.math.floor @Composable fun ForYouRoute( @@ -166,7 +171,25 @@ fun ForYouScreen( else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1) } + val lazyListState = rememberLazyListState() + JankMetricEffect(lazyListState) { metricsHolder -> + combine( + snapshotFlow { lazyListState.isScrollInProgress }, + snapshotFlow { lazyListState.firstVisibleItemIndex }, + ) { isScrollInProgress, firstVisibleItemIndex -> + if (isScrollInProgress) { + metricsHolder.state?.addState( + "ForYou:Feed:Scrolling", + "index=${firstVisibleItemIndex}" + ) + } else { + metricsHolder.state?.removeState("ForYou:Feed:Scrolling") + } + }.collect() + } + LazyColumn( + state = lazyListState, modifier = Modifier.fillMaxSize(), ) { InterestsSelection( From 5a1ece4f80219b349e9eca12fb64638c9a7ae412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 23 May 2022 14:52:03 +0200 Subject: [PATCH 13/13] Add ForYou TopicSelection scrolling state Change-Id: Icb85b612b1aa45fc5579acea9358a2da70de15d6 --- .../feature/foryou/AuthorsCarousel.kt | 2 +- .../feature/foryou/ForYouScreen.kt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt index 81024276d..bf1f5d5dc 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/AuthorsCarousel.kt @@ -77,7 +77,7 @@ fun AuthorsCarousel( } }.collect() } - + LazyRow(modifier, lazyListState) { items(items = authors, key = { item -> item.author.id }) { followableAuthor -> AuthorItem( diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index b29a3b6e0..8a3782a8b 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid 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.rememberLazyListState import androidx.compose.foundation.shape.CornerSize @@ -320,7 +321,25 @@ private fun TopicSelection( onTopicCheckedChanged: (String, Boolean) -> Unit, modifier: Modifier = Modifier ) { + val lazyGridState = rememberLazyGridState() + JankMetricEffect(lazyGridState) { metricsHolder -> + combine( + snapshotFlow { lazyGridState.isScrollInProgress }, + snapshotFlow { lazyGridState.firstVisibleItemIndex }, + ) { isScrollInProgress, firstVisibleItemIndex -> + if (isScrollInProgress) { + metricsHolder.state?.addState( + "ForYou:TopicSelection:Scrolling", + "index=${firstVisibleItemIndex}" + ) + } else { + metricsHolder.state?.removeState("ForYou:TopicSelection:Scrolling") + } + }.collect() + } + LazyHorizontalGrid( + state = lazyGridState, rows = GridCells.Fixed(3), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),