From 3ab9d2da5ff014528c415a030d6100cf91a221a4 Mon Sep 17 00:00:00 2001 From: Ajesh R Date: Tue, 4 Aug 2026 12:23:16 +0530 Subject: [PATCH 1/2] Migrate JankStatsSample from Fragments to Jetpack Compose and Navigation3 * Replace Fragment-based UI and Navigation Component with Jetpack Compose and Jetpack Navigation3. * Remove `MessageListFragment`, `MessageContentFragment`, `ComposeListFragment`, `MessageListAdapter`, and associated XML layout files and navigation graphs. * Add Jetpack Compose UI components (`MessageList`, `MessageContentScreen`, `JankStatsScaffold`) and Navigation3 components (`NavKeys`, `NavigationState`, `Navigator`). * Refactor `JankLoggingActivity` and `JankAggregatorActivity` to `ComponentActivity` using `setContent` with Compose UI and Lifecycle effects. * Bump `compileSdk` and `targetSdk` to 36, and `minSdk` to 23. * Add `navigation3` and `kotlinx-serialization` dependencies to Gradle configuration. --- JankStatsSample/app/build.gradle.kts | 15 +- .../app/src/main/AndroidManifest.xml | 8 +- .../jankstats/JankAggregatorActivity.kt | 157 ++++++++++------ .../example/jankstats/JankLoggingActivity.kt | 170 ++++++++++-------- .../java/com/example/jankstats/JankyView.kt | 41 ----- .../jankstats/MessageContentFragment.kt | 52 ------ .../example/jankstats/MessageListAdapter.kt | 58 ------ .../example/jankstats/MessageListFragment.kt | 84 --------- .../jankstats/compose/JankStatsScaffold.kt | 102 +++++++++++ .../jankstats/compose/JankStatsTheme.kt | 41 +++++ .../jankstats/compose/MessageContentScreen.kt | 59 ++++++ ...seListFragment.kt => MessageListScreen.kt} | 66 +++---- .../example/jankstats/navigation/NavKeys.kt | 29 +++ .../jankstats/navigation/NavigationState.kt | 94 ++++++++++ .../example/jankstats/navigation/Navigator.kt | 41 +++++ .../src/main/res/drawable/ic_arrow_back.xml | 10 ++ .../main/res/layout/activity_jank_logging.xml | 61 ------- .../res/layout/fragment_message_content.xml | 44 ----- .../main/res/layout/fragment_message_list.xml | 35 ---- .../app/src/main/res/layout/message_item.xml | 38 ---- .../app/src/main/res/navigation/nav_graph.xml | 55 ------ .../app/src/main/res/values/strings.xml | 12 +- JankStatsSample/build.gradle | 4 +- JankStatsSample/gradle/libs.versions.toml | 9 +- 24 files changed, 629 insertions(+), 656 deletions(-) delete mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/JankyView.kt delete mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/MessageContentFragment.kt delete mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/MessageListAdapter.kt delete mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/MessageListFragment.kt create mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt create mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt create mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt rename JankStatsSample/app/src/main/java/com/example/jankstats/compose/{ComposeListFragment.kt => MessageListScreen.kt} (62%) create mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavKeys.kt create mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavigationState.kt create mode 100644 JankStatsSample/app/src/main/java/com/example/jankstats/navigation/Navigator.kt create mode 100644 JankStatsSample/app/src/main/res/drawable/ic_arrow_back.xml delete mode 100644 JankStatsSample/app/src/main/res/layout/activity_jank_logging.xml delete mode 100644 JankStatsSample/app/src/main/res/layout/fragment_message_content.xml delete mode 100644 JankStatsSample/app/src/main/res/layout/fragment_message_list.xml delete mode 100644 JankStatsSample/app/src/main/res/layout/message_item.xml delete mode 100644 JankStatsSample/app/src/main/res/navigation/nav_graph.xml diff --git a/JankStatsSample/app/build.gradle.kts b/JankStatsSample/app/build.gradle.kts index b89c70bd..98f3d026 100644 --- a/JankStatsSample/app/build.gradle.kts +++ b/JankStatsSample/app/build.gradle.kts @@ -18,20 +18,20 @@ plugins { id("com.android.application") alias(libs.plugins.kotlin) alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) } android { - compileSdk = 35 + compileSdk = 36 buildFeatures { - viewBinding = true compose = true } defaultConfig { applicationId = "com.example.jankstats" - minSdk = 21 - targetSdk = 35 + minSdk = 23 + targetSdk = 36 versionCode = 1 versionName = "1.0" @@ -62,13 +62,16 @@ dependencies { implementation(composeBom) implementation(libs.activity) implementation(libs.appcompat) + implementation(libs.compose.activity) + implementation("androidx.compose.material:material-icons-core") implementation(libs.compose.material) implementation(libs.compose.ui) implementation(libs.compose.ui.tooling) implementation(libs.constraintlayout) implementation(libs.jankstats) implementation(libs.material) - implementation(libs.navigation.fragment) - implementation(libs.navigation.ui) + implementation(libs.navigation3.runtime) + implementation(libs.navigation3.ui) + implementation(libs.kotlinx.serialization.json) implementation(libs.tracing) } diff --git a/JankStatsSample/app/src/main/AndroidManifest.xml b/JankStatsSample/app/src/main/AndroidManifest.xml index a9dcf24f..a896d9ed 100644 --- a/JankStatsSample/app/src/main/AndroidManifest.xml +++ b/JankStatsSample/app/src/main/AndroidManifest.xml @@ -11,16 +11,18 @@ + android:exported="true" + android:windowSoftInputMode="adjustResize"> - + android:exported="true" + android:windowSoftInputMode="adjustResize"> + diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt index eeb59b82..5da4bedc 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt @@ -18,14 +18,33 @@ package com.example.jankstats import android.os.Bundle import android.util.Log -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.metrics.performance.PerformanceMetricsState -import androidx.navigation.NavController -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.navigateUp -import androidx.navigation.ui.setupActionBarWithNavController -import com.example.jankstats.databinding.ActivityJankLoggingBinding +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.jankstats.compose.JankStatsScaffold +import com.example.jankstats.compose.JankStatsTheme +import com.example.jankstats.compose.MessageContentScreen +import com.example.jankstats.compose.MessageList +import com.example.jankstats.compose.rememberMetricsStateHolder +import com.example.jankstats.navigation.ComposeListRoute +import com.example.jankstats.navigation.MessageContentRoute +import com.example.jankstats.navigation.MessageListRoute +import com.example.jankstats.navigation.Navigator +import com.example.jankstats.navigation.rememberNavigationState +import com.example.jankstats.navigation.toEntries /** * This activity shows how to use JankStatsAggregator, a class in this test directory layered @@ -35,15 +54,10 @@ import com.example.jankstats.databinding.ActivityJankLoggingBinding * or if JankStatsAggregator issues the report itself. */ // [START aggregator_activity_init] -class JankAggregatorActivity : AppCompatActivity() { +class JankAggregatorActivity : ComponentActivity() { private lateinit var jankStatsAggregator: JankStatsAggregator - // [START_EXCLUDE silent] - private lateinit var binding: ActivityJankLoggingBinding - private lateinit var navController: NavController - private lateinit var appBarConfiguration: AppBarConfiguration - // [START jank_aggregator_listener] private val jankReportListener = JankStatsAggregator.OnJankReportListener { reason, totalFrames, jankFrameData -> @@ -65,48 +79,91 @@ class JankAggregatorActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // [START_EXCLUDE] - binding = ActivityJankLoggingBinding.inflate(layoutInflater) - setContentView(binding.root) - setupUi() - // [END_EXCLUDE] - // Metrics state holder can be retrieved regardless of JankStats initialization. - val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root) - - // Initialize JankStats with an aggregator for the current window. - jankStatsAggregator = JankStatsAggregator(window, jankReportListener) - - // Add the Activity name as state. - metricsStateHolder.state?.putState("Activity", javaClass.simpleName) - } - // [END aggregator_activity_init] + enableEdgeToEdge() + setContent { + jankStatsAggregator = remember { + JankStatsAggregator(window, jankReportListener) + } + // ... + // metrics state holder can be retrieved regardless of JankStats initialization + val metricsStateHolder = rememberMetricsStateHolder() + // ... + // ... + // add activity name as state + metricsStateHolder.state?.putState("Activity", javaClass.simpleName) + + LifecycleResumeEffect(jankStatsAggregator) { + jankStatsAggregator.jankStats.isTrackingEnabled = true + onPauseOrDispose { + jankStatsAggregator.issueJankReport("Activity paused") + jankStatsAggregator.jankStats.isTrackingEnabled = false + } + } - // [START aggregator_tracking_enabled] - override fun onResume() { - super.onResume() - jankStatsAggregator.jankStats.isTrackingEnabled = true - } + val listState = rememberLazyListState() + LaunchedEffect(metricsStateHolder, listState) { + snapshotFlow { listState.isScrollInProgress }.collect { isScrolling -> + if (isScrolling) { + metricsStateHolder.state?.putState("LazyList", "Scrolling") + } else { + metricsStateHolder.state?.removeState("LazyList") + } + } + } - override fun onPause() { - super.onPause() - // Before disabling tracking, issue the report with (optionally) specified reason. - jankStatsAggregator.issueJankReport("Activity paused") - jankStatsAggregator.jankStats.isTrackingEnabled = false - } - // [END aggregator_tracking_enabled] + val topLevelRoutes = remember { setOf(MessageListRoute, ComposeListRoute) } + val navigationState = rememberNavigationState( + startRoute = MessageListRoute, + topLevelRoutes = topLevelRoutes + ) + val navigator = remember(navigationState) { Navigator(navigationState) } - override fun onSupportNavigateUp(): Boolean { - return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() - } + val currentStack = navigationState.backStacks[navigationState.topLevelRoute] + val activeKey = currentStack?.lastOrNull() ?: navigationState.topLevelRoute + val canNavigateUp = currentStack != null && currentStack.size > 1 - private fun setupUi() { - setSupportActionBar(binding.toolbar) + val title = when (activeKey) { + is MessageListRoute -> "Message List" + is ComposeListRoute -> "Compose List" + is MessageContentRoute -> "Message Content" + else -> "JankStats Sample" + } - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.navigation_container) as NavHostFragment - navController = navHostFragment.navController + val entryProvider = remember(navigator) { + entryProvider { + entry { + MessageList(onItemClick = { headerText -> + navigator.navigate(MessageContentRoute(headerText)) + }) + } + entry { + MessageList(onItemClick = { headerText -> + navigator.navigate(MessageContentRoute(headerText)) + }) + } + entry { route -> + MessageContentScreen(title = route.title) + } + } + } - appBarConfiguration = AppBarConfiguration(navController.graph) - setupActionBarWithNavController(navController, appBarConfiguration) + JankStatsTheme { + JankStatsScaffold( + title = title, + canNavigateUp = canNavigateUp, + currentTopLevelRoute = navigationState.topLevelRoute, + onNavigateUp = { navigator.goBack() }, + onBottomTabSelected = { navKey -> + navigator.navigate(navKey) + } + ) { innerPadding -> + NavDisplay( + entries = navigationState.toEntries(entryProvider), + onBack = { navigator.goBack() }, + modifier = Modifier.padding(innerPadding) + ) + } + } + } } } diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt index 797fe3ae..fed97321 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt @@ -18,33 +18,39 @@ package com.example.jankstats import android.os.Bundle import android.util.Log -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.metrics.performance.JankStats import androidx.metrics.performance.PerformanceMetricsState -import androidx.navigation.NavController -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.navigateUp -import androidx.navigation.ui.setupActionBarWithNavController -import androidx.navigation.ui.setupWithNavController -import com.example.jankstats.databinding.ActivityJankLoggingBinding +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import com.example.jankstats.compose.JankStatsScaffold +import com.example.jankstats.compose.JankStatsTheme +import com.example.jankstats.compose.MessageContentScreen +import com.example.jankstats.compose.MessageList +import com.example.jankstats.compose.rememberMetricsStateHolder +import com.example.jankstats.navigation.ComposeListRoute +import com.example.jankstats.navigation.MessageContentRoute +import com.example.jankstats.navigation.MessageListRoute +import com.example.jankstats.navigation.Navigator +import com.example.jankstats.navigation.rememberNavigationState +import com.example.jankstats.navigation.toEntries /** * This activity shows the basic usage of JankStats, from creating and enabling it to track * a view hierarchy, to setting application state on JankStats, to receiving and logging per-frame * callbacks with jank data. */ -// [START activity_init] -class JankLoggingActivity : AppCompatActivity() { +class JankLoggingActivity : ComponentActivity() { private lateinit var jankStats: JankStats - // [START_EXCLUDE silent] - private lateinit var binding: ActivityJankLoggingBinding - private lateinit var navController: NavController - private lateinit var appBarConfiguration: AppBarConfiguration - - // [START jank_frame_listener] private val jankFrameListener = JankStats.OnFrameListener { frameData -> // A real app could do something more interesting, like writing the info to local storage and later on report it. Log.v("JankStatsSample", frameData.toString()) @@ -54,65 +60,81 @@ class JankLoggingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // [START_EXCLUDE] - binding = ActivityJankLoggingBinding.inflate(layoutInflater) - setContentView(binding.root) - setupUi() - // [END_EXCLUDE] - // metrics state holder can be retrieved regardless of JankStats initialization - val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root) - - // initialize JankStats for current window - jankStats = JankStats.createAndTrack(window, jankFrameListener) - - // add activity name as state - metricsStateHolder.state?.putState("Activity", javaClass.simpleName) - // [START_EXCLUDE] - setupNavigationState() - // [END_EXCLUDE] - } - // [END activity_init] - - // [START tracking_enabled] - override fun onResume() { - super.onResume() - jankStats.isTrackingEnabled = true - } - - override fun onPause() { - super.onPause() - jankStats.isTrackingEnabled = false - } - // [END tracking_enabled] - - override fun onSupportNavigateUp(): Boolean { - return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() - } - - private fun setupUi() { - setSupportActionBar(binding.toolbar) - - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.navigation_container) as NavHostFragment - navController = navHostFragment.navController - - binding.bottomNavigation.setupWithNavController(navController) - - appBarConfiguration = AppBarConfiguration(navController.graph) - - setupActionBarWithNavController(navController, appBarConfiguration) - } - - private fun setupNavigationState() { - // [START state_navigation] - val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root) - // add current navigation information into JankStats state - navController.addOnDestinationChangedListener { _, destination, arguments -> - metricsStateHolder.state?.putState( - "Navigation", - "Args(${arguments.toString()}), $destination" + enableEdgeToEdge() + + setContent { + jankStats = remember { + JankStats.createAndTrack(window, jankFrameListener) + } + // ... + // metrics state holder can be retrieved regardless of JankStats initialization + val metricsStateHolder = rememberMetricsStateHolder() + // ... + // ... + // add activity name as state + metricsStateHolder.state?.putState("Activity", javaClass.simpleName) + + LifecycleResumeEffect(jankStats) { + jankStats.isTrackingEnabled = true + onPauseOrDispose { + Log.e("Activity Paused,","Tracking") + jankStats.isTrackingEnabled = false + } + } + + val topLevelRoutes = remember { setOf(MessageListRoute, ComposeListRoute) } + val navigationState = rememberNavigationState( + startRoute = MessageListRoute, + topLevelRoutes = topLevelRoutes ) + val navigator = remember(navigationState) { Navigator(navigationState) } + + val currentStack = navigationState.backStacks[navigationState.topLevelRoute] + val activeKey = currentStack?.lastOrNull() ?: navigationState.topLevelRoute + val canNavigateUp = currentStack != null && currentStack.size > 1 + + val title = when (activeKey) { + is MessageListRoute -> "Message List" + is ComposeListRoute -> "Compose List" + is MessageContentRoute -> "Message Content" + else -> "JankStats Sample" + } + + val entryProvider = remember(navigator) { + entryProvider { + entry { + MessageList(onItemClick = { headerText -> + navigator.navigate(MessageContentRoute(headerText)) + }) + } + entry { + MessageList(onItemClick = { headerText -> + navigator.navigate(MessageContentRoute(headerText)) + }) + } + entry { route -> + MessageContentScreen(title = route.title) + } + } + } + + JankStatsTheme { + JankStatsScaffold( + title = title, + canNavigateUp = canNavigateUp, + currentTopLevelRoute = navigationState.topLevelRoute, + onNavigateUp = { navigator.goBack() }, + onBottomTabSelected = { navKey -> + navigator.navigate(navKey) + } + ) { innerPadding -> + NavDisplay( + entries = navigationState.toEntries(entryProvider), + onBack = { navigator.goBack() }, + modifier = Modifier.padding(innerPadding) + ) + } + } } - // [END state_navigation] } } diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/JankyView.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/JankyView.kt deleted file mode 100644 index 08de49cb..00000000 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/JankyView.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2021 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 - * - * http://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.example.jankstats - -import android.content.Context -import android.graphics.Canvas -import android.util.AttributeSet -import android.view.View -import com.example.jankstats.tools.simulateJank - -/** - * This custom view is used to inject an artificial, random delay during drawing, to simulate - * jank on the UI thread. - */ -class JankyView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - defStyleRes: Int = 0 -) : View(context, attrs, defStyleAttr, defStyleRes) { - - override fun onDraw(canvas: Canvas) { - simulateJank() - super.onDraw(canvas) - } - -} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/MessageContentFragment.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/MessageContentFragment.kt deleted file mode 100644 index 329a24a2..00000000 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/MessageContentFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2021 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 - * - * http://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.example.jankstats - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.example.jankstats.databinding.FragmentMessageContentBinding - -/** - * A simple [Fragment] subclass. - */ -class MessageContentFragment : Fragment() { - - private var _binding: FragmentMessageContentBinding? = null - private val binding get() = requireNotNull(_binding) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - // Inflate the layout for this fragment - _binding = FragmentMessageContentBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.messageHeader.text = arguments?.getString("title") - } - - override fun onDestroyView() { - _binding = null - super.onDestroyView() - } -} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/MessageListAdapter.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/MessageListAdapter.kt deleted file mode 100644 index 6aa4c0a0..00000000 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/MessageListAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2021 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 - * - * http://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.example.jankstats - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.navigation.Navigation -import androidx.recyclerview.widget.RecyclerView -import com.example.jankstats.databinding.MessageItemBinding - -class MessageListAdapter( - private val messageList: Array -) : RecyclerView.Adapter() { - - class MessageHeaderViewHolder( - private val binding: MessageItemBinding - ) : RecyclerView.ViewHolder(binding.root) { - - fun bind(headerText: String) { - itemView.setOnClickListener { - val bundle = bundleOf("title" to headerText) - Navigation.findNavController(it).navigate( - R.id.action_messageList_to_messageContent, bundle - ) - } - binding.messageHeader.text = headerText - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageHeaderViewHolder { - val inflater = LayoutInflater.from(parent.context) - val itemBinding = MessageItemBinding.inflate(inflater, parent, false) - return MessageHeaderViewHolder(itemBinding) - } - - override fun onBindViewHolder(holder: MessageHeaderViewHolder, position: Int) { - holder.bind(messageList[position]) - } - - override fun getItemCount(): Int { - return messageList.size - } -} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/MessageListFragment.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/MessageListFragment.kt deleted file mode 100644 index b706b941..00000000 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/MessageListFragment.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2021 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 - * - * http://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.example.jankstats - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.metrics.performance.PerformanceMetricsState -import androidx.recyclerview.widget.RecyclerView -import com.example.jankstats.databinding.FragmentMessageListBinding - -/** - * A simple [Fragment] subclass as the second destination in the navigation. - */ -class MessageListFragment : Fragment() { - - private var _binding: FragmentMessageListBinding? = null - private val binding get() = requireNotNull(_binding) - - // initialized when View is created - private var metricsStateHolder: PerformanceMetricsState.Holder? = null - - private val messageList = Array(100) { "Message #$it" } - - // [START state_recyclerview] - private val scrollListener = object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - // check if JankStats is initialized and skip adding state if not - val metricsState = metricsStateHolder?.state ?: return - - when (newState) { - RecyclerView.SCROLL_STATE_DRAGGING -> { - metricsState.putState("RecyclerView", "Dragging") - } - RecyclerView.SCROLL_STATE_SETTLING -> { - metricsState.putState("RecyclerView", "Settling") - } - else -> { - metricsState.removeState("RecyclerView") - } - } - } - } - // [END state_recyclerview] - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentMessageListBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(view) - - binding.messageList.adapter = MessageListAdapter(messageList) - binding.messageList.addOnScrollListener(scrollListener) - } - - override fun onDestroyView() { - _binding = null - metricsStateHolder = null - super.onDestroyView() - } - -} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt new file mode 100644 index 00000000..e44f03b6 --- /dev/null +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2026 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 + * + * http://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.example.jankstats.compose + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.navigation3.runtime.NavKey +import com.example.jankstats.R +import com.example.jankstats.navigation.ComposeListRoute +import com.example.jankstats.navigation.MessageListRoute + +@Composable +fun JankStatsScaffold( + title: String, + canNavigateUp: Boolean, + currentTopLevelRoute: NavKey?, + onNavigateUp: () -> Unit, + onBottomTabSelected: (NavKey) -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = title) }, + navigationIcon = if (canNavigateUp) { + { + IconButton(onClick = onNavigateUp) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back), + contentDescription = "Navigate Up" + ) + } + } + } else null + ) + }, + bottomBar = { + BottomNavigation { + BottomNavigationItem( + selected = currentTopLevelRoute == MessageListRoute, + onClick = { onBottomTabSelected(MessageListRoute) }, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_views), + contentDescription = stringResource(id = R.string.views) + ) + }, + label = { Text(text = stringResource(id = R.string.views)) } + ) + BottomNavigationItem( + selected = currentTopLevelRoute == ComposeListRoute, + onClick = { onBottomTabSelected(ComposeListRoute) }, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_compose), + contentDescription = stringResource(id = R.string.compose) + ) + }, + label = { Text(text = stringResource(id = R.string.compose)) } + ) + BottomNavigationItem( + selected = false, + onClick = { /* No-op / Not implemented */ }, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_construction), + contentDescription = stringResource(id = R.string.not_implemented) + ) + }, + label = { Text(text = stringResource(id = R.string.not_implemented)) } + ) + } + } + ) { innerPadding -> + content(innerPadding) + } +} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt new file mode 100644 index 00000000..034a73f6 --- /dev/null +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 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 + * + * http://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.example.jankstats.compose + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val Purple200 = Color(0xFFBB86FC) +private val Purple500 = Color(0xFF6200EE) +private val Purple700 = Color(0xFF3700B3) +private val Teal200 = Color(0xFF03DAC5) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 +) + +@Composable +fun JankStatsTheme(content: @Composable () -> Unit) { + MaterialTheme( + colors = LightColorPalette, + content = content + ) +} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt new file mode 100644 index 00000000..85b83d6d --- /dev/null +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2026 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 + * + * http://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.example.jankstats.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun MessageContentScreen(title: String?) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = title ?: "Hello blank screen", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + Text( + text = "Message content placeholder", + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Preview(showBackground = true) +@Composable +fun MessageContentScreenPreview() { + JankStatsTheme { + MessageContentScreen(title = "Message #1") + } +} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/ComposeListFragment.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageListScreen.kt similarity index 62% rename from JankStatsSample/app/src/main/java/com/example/jankstats/compose/ComposeListFragment.kt rename to JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageListScreen.kt index 3b3cb4d8..b837a247 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/ComposeListFragment.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageListScreen.kt @@ -16,16 +16,17 @@ package com.example.jankstats.compose -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -33,42 +34,15 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import androidx.navigation.findNavController -import com.example.jankstats.R import com.example.jankstats.tools.simulateJank -/** - * Showcase how to work with JankStats from Compose. - * This Fragment will intentionally cause poor UI performance which can be monitored by JankStats. - */ -class ComposeListFragment : Fragment() { - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner) - ) - - setContent { - MaterialTheme { - MessageList(onItemClick = { - findNavController().navigate(R.id.action_composeList_to_messageContent) - }) - } - } - } - } -} - @Composable -fun MessageList(onItemClick: () -> Unit) { +fun MessageList( + onItemClick: (String) -> Unit, + contentPadding: PaddingValues = PaddingValues(0.dp) +) { val listState = rememberLazyListState() // [START compose_jank_metrics] val metricsStateHolder = rememberMetricsStateHolder() @@ -85,25 +59,27 @@ fun MessageList(onItemClick: () -> Unit) { } // [END compose_jank_metrics] - - LazyColumn(state = listState) { + LazyColumn( + state = listState, + contentPadding = contentPadding + ) { items(100) { index -> MessageItem(index, onItemClick) } } } - @Composable -fun MessageItem(item: Int, onItemClick: () -> Unit) { +fun MessageItem(item: Int, onItemClick: (String) -> Unit) { + val headerText = "Message #$item" Row( Modifier .fillMaxWidth() - .clickable(onClick = onItemClick) + .clickable(onClick = { onItemClick(headerText) }) .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - Text("Message #$item", Modifier.padding(end = 16.dp)) + Text(headerText, Modifier.padding(end = 16.dp)) JankyComposable() } } @@ -123,5 +99,7 @@ fun JankyComposable() { @Preview(widthDp = 500) @Composable fun MessageListPreview() { - MessageList(onItemClick = {}) + JankStatsTheme { + MessageList(onItemClick = {}) + } } diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavKeys.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavKeys.kt new file mode 100644 index 00000000..a3f07c9c --- /dev/null +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavKeys.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 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 + * + * http://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.example.jankstats.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +data object MessageListRoute : NavKey + +@Serializable +data object ComposeListRoute : NavKey + +@Serializable +data class MessageContentRoute(val title: String) : NavKey diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavigationState.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavigationState.kt new file mode 100644 index 00000000..d7fceafe --- /dev/null +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/NavigationState.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2026 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 + * + * http://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.example.jankstats.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSerializable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.rememberDecoratedNavEntries +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.runtime.serialization.NavKeySerializer +import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer + +@Composable +fun rememberNavigationState( + startRoute: NavKey, + topLevelRoutes: Set +): NavigationState { + + val topLevelRoute = rememberSerializable( + startRoute, topLevelRoutes, + serializer = MutableStateSerializer(NavKeySerializer()) + ) { + mutableStateOf(startRoute) + } + + val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } + + return remember(startRoute, topLevelRoutes) { + NavigationState( + startRoute = startRoute, + topLevelRoute = topLevelRoute, + backStacks = backStacks + ) + } +} + +class NavigationState( + val startRoute: NavKey, + topLevelRoute: MutableState, + val backStacks: Map> +) { + var topLevelRoute: NavKey by topLevelRoute + val stacksInUse: List + get() = if (topLevelRoute == startRoute) { + listOf(startRoute) + } else { + listOf(startRoute, topLevelRoute) + } +} + +@Composable +fun NavigationState.toEntries( + entryProvider: (NavKey) -> NavEntry +): SnapshotStateList> { + + val decoratedEntries = backStacks.mapValues { (_, stack) -> + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + ) + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = decorators, + entryProvider = entryProvider + ) + } + + return stacksInUse + .flatMap { decoratedEntries[it] ?: emptyList() } + .toMutableStateList() +} diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/Navigator.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/Navigator.kt new file mode 100644 index 00000000..7b1e26a6 --- /dev/null +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/navigation/Navigator.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 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 + * + * http://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.example.jankstats.navigation + +import androidx.navigation3.runtime.NavKey + +class Navigator(val state: NavigationState) { + fun navigate(route: NavKey) { + if (route in state.backStacks.keys) { + state.topLevelRoute = route + } else { + state.backStacks[state.topLevelRoute]?.add(route) + } + } + + fun goBack() { + val currentStack = state.backStacks[state.topLevelRoute] + ?: error("Stack for ${state.topLevelRoute} not found") + val currentRoute = currentStack.last() + + if (currentRoute == state.topLevelRoute) { + state.topLevelRoute = state.startRoute + } else { + currentStack.removeLastOrNull() + } + } +} diff --git a/JankStatsSample/app/src/main/res/drawable/ic_arrow_back.xml b/JankStatsSample/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 00000000..bab545a7 --- /dev/null +++ b/JankStatsSample/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/JankStatsSample/app/src/main/res/layout/activity_jank_logging.xml b/JankStatsSample/app/src/main/res/layout/activity_jank_logging.xml deleted file mode 100644 index 75724d35..00000000 --- a/JankStatsSample/app/src/main/res/layout/activity_jank_logging.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/JankStatsSample/app/src/main/res/layout/fragment_message_content.xml b/JankStatsSample/app/src/main/res/layout/fragment_message_content.xml deleted file mode 100644 index a99fe724..00000000 --- a/JankStatsSample/app/src/main/res/layout/fragment_message_content.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - diff --git a/JankStatsSample/app/src/main/res/layout/fragment_message_list.xml b/JankStatsSample/app/src/main/res/layout/fragment_message_list.xml deleted file mode 100644 index 1d545e30..00000000 --- a/JankStatsSample/app/src/main/res/layout/fragment_message_list.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - diff --git a/JankStatsSample/app/src/main/res/layout/message_item.xml b/JankStatsSample/app/src/main/res/layout/message_item.xml deleted file mode 100644 index 4363c15a..00000000 --- a/JankStatsSample/app/src/main/res/layout/message_item.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - diff --git a/JankStatsSample/app/src/main/res/navigation/nav_graph.xml b/JankStatsSample/app/src/main/res/navigation/nav_graph.xml deleted file mode 100644 index 0f456b34..00000000 --- a/JankStatsSample/app/src/main/res/navigation/nav_graph.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/JankStatsSample/app/src/main/res/values/strings.xml b/JankStatsSample/app/src/main/res/values/strings.xml index 7505a51b..51ec9701 100644 --- a/JankStatsSample/app/src/main/res/values/strings.xml +++ b/JankStatsSample/app/src/main/res/values/strings.xml @@ -17,16 +17,16 @@ JankStats Settings - - First Fragment - Second Fragment + + First Screen + Second Screen Next Previous - Hello first fragment - Hello second fragment. Arg: %1$s + Hello first screen + Hello second screen. Arg: %1$s - Hello blank fragment + Hello blank screen Views Compose diff --git a/JankStatsSample/build.gradle b/JankStatsSample/build.gradle index 9cb821a6..7fe4686c 100644 --- a/JankStatsSample/build.gradle +++ b/JankStatsSample/build.gradle @@ -18,5 +18,5 @@ plugins { alias(libs.plugins.application) apply false alias(libs.plugins.kotlin) apply false alias(libs.plugins.compose.compiler) apply false - -} \ No newline at end of file + alias(libs.plugins.kotlin.serialization) apply false +} diff --git a/JankStatsSample/gradle/libs.versions.toml b/JankStatsSample/gradle/libs.versions.toml index 3ff0f0a5..61393622 100644 --- a/JankStatsSample/gradle/libs.versions.toml +++ b/JankStatsSample/gradle/libs.versions.toml @@ -23,8 +23,9 @@ constraint_layout = "2.2.1" core = "1.9.0" jankstats = "1.0.0-beta02" kotlin = "2.2.0" +kotlinxSerialization = "1.8.0" material = "1.12.0" -navigation = "2.9.3" +nav3 = "1.0.0" tracing = "1.3.0" [libraries] @@ -40,9 +41,10 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo jankstats = { group = "androidx.metrics", name = "metrics-performance", version.ref = "jankstats" } kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } material = { group = "com.google.android.material", name = "material", version.ref = "material"} -navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation"} -navigation-ui = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation"} +navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "nav3" } +navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "nav3" } tracing = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "tracing"} [plugins] @@ -50,4 +52,5 @@ tracing = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "tra application = { id = "com.android.application", version.ref = "agp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } test = { id = "com.android.test", version.ref = "agp" } From a2e6dff40c51a192497557d76b3b2914bfba09e4 Mon Sep 17 00:00:00 2001 From: Ajesh R Date: Wed, 5 Aug 2026 09:48:00 +0530 Subject: [PATCH 2/2] Migrate JankStatsSample to Material 3 and update dependencies * Migrate UI components, scaffold, and theme from Compose Material to Material 3. * Update project dependencies including Kotlin, AGP, Compose BOM, JankStats, and Navigation3. * Upgrade Java target and source compatibility to Java 21. * Enable code minification (`isMinifyEnabled`) and resource shrinking (`isShrinkResources`) for release builds. * Move `android.intent.action.MAIN` intent filter to `JankAggregatorActivity`. * Remove unused dependencies and unused imports across the sample app. --- JankStatsSample/app/build.gradle.kts | 16 +++--- .../app/src/main/AndroidManifest.xml | 2 +- .../jankstats/JankAggregatorActivity.kt | 4 -- .../example/jankstats/JankLoggingActivity.kt | 3 +- .../jankstats/compose/JankStatsScaffold.kt | 32 +++++------ .../jankstats/compose/JankStatsTheme.kt | 53 ++++++++++++++----- .../jankstats/compose/MessageContentScreen.kt | 2 +- .../jankstats/compose/MessageListScreen.kt | 2 +- .../app/src/main/res/values/themes.xml | 16 +----- JankStatsSample/gradle/libs.versions.toml | 25 ++++----- 10 files changed, 79 insertions(+), 76 deletions(-) diff --git a/JankStatsSample/app/build.gradle.kts b/JankStatsSample/app/build.gradle.kts index 98f3d026..e007793f 100644 --- a/JankStatsSample/app/build.gradle.kts +++ b/JankStatsSample/app/build.gradle.kts @@ -40,17 +40,16 @@ android { buildTypes { getByName("release") { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } + composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } @@ -63,11 +62,10 @@ dependencies { implementation(libs.activity) implementation(libs.appcompat) implementation(libs.compose.activity) - implementation("androidx.compose.material:material-icons-core") - implementation(libs.compose.material) + implementation(libs.material.icons.core) + implementation(libs.material3) implementation(libs.compose.ui) implementation(libs.compose.ui.tooling) - implementation(libs.constraintlayout) implementation(libs.jankstats) implementation(libs.material) implementation(libs.navigation3.runtime) diff --git a/JankStatsSample/app/src/main/AndroidManifest.xml b/JankStatsSample/app/src/main/AndroidManifest.xml index a896d9ed..3dcb94df 100644 --- a/JankStatsSample/app/src/main/AndroidManifest.xml +++ b/JankStatsSample/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:exported="true" android:windowSoftInputMode="adjustResize"> + @@ -22,7 +23,6 @@ android:exported="true" android:windowSoftInputMode="adjustResize"> - diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt index 5da4bedc..198d0095 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/JankAggregatorActivity.kt @@ -23,14 +23,11 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.metrics.performance.PerformanceMetricsState import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay @@ -88,7 +85,6 @@ class JankAggregatorActivity : ComponentActivity() { // metrics state holder can be retrieved regardless of JankStats initialization val metricsStateHolder = rememberMetricsStateHolder() // ... - // ... // add activity name as state metricsStateHolder.state?.putState("Activity", javaClass.simpleName) diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt index fed97321..6588dfe2 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/JankLoggingActivity.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.metrics.performance.JankStats -import androidx.metrics.performance.PerformanceMetricsState import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay @@ -77,7 +76,7 @@ class JankLoggingActivity : ComponentActivity() { LifecycleResumeEffect(jankStats) { jankStats.isTrackingEnabled = true onPauseOrDispose { - Log.e("Activity Paused,","Tracking") + Log.v("Activity Paused,","Tracking jank stats is disabled") jankStats.isTrackingEnabled = false } } diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt index e44f03b6..8b7aa0bf 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsScaffold.kt @@ -17,16 +17,15 @@ package com.example.jankstats.compose import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.material.BottomNavigation -import androidx.compose.material.BottomNavigationItem -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.navigation3.runtime.NavKey @@ -34,6 +33,7 @@ import com.example.jankstats.R import com.example.jankstats.navigation.ComposeListRoute import com.example.jankstats.navigation.MessageListRoute +@OptIn(ExperimentalMaterial3Api::class) @Composable fun JankStatsScaffold( title: String, @@ -47,8 +47,8 @@ fun JankStatsScaffold( topBar = { TopAppBar( title = { Text(text = title) }, - navigationIcon = if (canNavigateUp) { - { + navigationIcon = { + if (canNavigateUp) { IconButton(onClick = onNavigateUp) { Icon( painter = painterResource(id = R.drawable.ic_arrow_back), @@ -56,12 +56,12 @@ fun JankStatsScaffold( ) } } - } else null + } ) }, bottomBar = { - BottomNavigation { - BottomNavigationItem( + NavigationBar { + NavigationBarItem( selected = currentTopLevelRoute == MessageListRoute, onClick = { onBottomTabSelected(MessageListRoute) }, icon = { @@ -72,7 +72,7 @@ fun JankStatsScaffold( }, label = { Text(text = stringResource(id = R.string.views)) } ) - BottomNavigationItem( + NavigationBarItem( selected = currentTopLevelRoute == ComposeListRoute, onClick = { onBottomTabSelected(ComposeListRoute) }, icon = { @@ -83,7 +83,7 @@ fun JankStatsScaffold( }, label = { Text(text = stringResource(id = R.string.compose)) } ) - BottomNavigationItem( + NavigationBarItem( selected = false, onClick = { /* No-op / Not implemented */ }, icon = { diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt index 034a73f6..53491dc4 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/JankStatsTheme.kt @@ -16,26 +16,55 @@ package com.example.jankstats.compose -import androidx.compose.material.MaterialTheme -import androidx.compose.material.lightColors +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext -private val Purple200 = Color(0xFFBB86FC) -private val Purple500 = Color(0xFF6200EE) -private val Purple700 = Color(0xFF3700B3) -private val Teal200 = Color(0xFF03DAC5) +private val Purple80 = Color(0xFFD0BCFF) +private val PurpleGrey80 = Color(0xFFCCC2DC) +private val Pink80 = Color(0xFFEFB8C8) -private val LightColorPalette = lightColors( - primary = Purple500, - primaryVariant = Purple700, - secondary = Teal200 +private val Purple40 = Color(0xFF6650a4) +private val PurpleGrey40 = Color(0xFF625b71) +private val Pink40 = Color(0xFF7D5260) + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 ) @Composable -fun JankStatsTheme(content: @Composable () -> Unit) { +fun JankStatsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + MaterialTheme( - colors = LightColorPalette, + colorScheme = colorScheme, content = content ) } diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt index 85b83d6d..590f7f2f 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageContentScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight diff --git a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageListScreen.kt b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageListScreen.kt index b837a247..26d7fdb6 100644 --- a/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageListScreen.kt +++ b/JankStatsSample/app/src/main/java/com/example/jankstats/compose/MessageListScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow diff --git a/JankStatsSample/app/src/main/res/values/themes.xml b/JankStatsSample/app/src/main/res/values/themes.xml index 60c47c0a..e54faa0b 100644 --- a/JankStatsSample/app/src/main/res/values/themes.xml +++ b/JankStatsSample/app/src/main/res/values/themes.xml @@ -1,20 +1,6 @@ - +