Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 178 additions & 102 deletions Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.jetsnack.model.Filter
Expand Down Expand Up @@ -108,7 +111,12 @@ private fun SnackCollectionList(
sharedTransitionScope: SharedTransitionScope,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
LazyColumn(
modifier = modifier.semantics {
testTag = "feed_list"
testTagsAsResourceId = true
},
) {
item {
Spacer(
Modifier.windowInsetsTopHeight(
Expand Down
62 changes: 62 additions & 0 deletions Jetsnack/benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.test)
}

android {
compileSdk = libs.versions.compileSdk.get().toInt()
namespace = "com.example.jetsnack.benchmark"

defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
// This benchmark build type is matching the type in the target application
create("benchmark") {
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
}
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.fromTarget("17")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}

dependencies {
implementation(libs.androidx.test.rules)
implementation(libs.androidx.test.runner)
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}
3 changes: 3 additions & 0 deletions Jetsnack/benchmark/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.jetsnack.benchmark

import android.content.Intent
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class JetsnackBenchmarks {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

private val targetPackageName = "com.example.jetsnack"

@Test
fun scrollFeedFrameTiming() = benchmarkRule.measureRepeated(
packageName = targetPackageName,
metrics = listOf(FrameTimingMetric()),
compilationMode = CompilationMode.DEFAULT,
startupMode = StartupMode.COLD,
iterations = 10,
setupBlock = {
pressHome()
val context = InstrumentationRegistry.getInstrumentation().context
val intent = context.packageManager.getLaunchIntentForPackage(packageName)!!
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
device.wait(Until.hasObject(By.pkg(packageName).depth(0)), 15000)
device.wait(Until.hasObject(By.res("feed_list")), 15000)
},
) {
val listSelector = By.res("feed_list")

// Scroll down through feed
repeat(3) {
val list = device.findObject(listSelector) ?: device.findObject(By.scrollable(true))
requireNotNull(list) { "Feed list not found!" }
list.setGestureMargin(device.displayWidth / 5)
list.fling(Direction.DOWN)
device.waitForIdle()
}

// Scroll back up
repeat(3) {
val list = device.findObject(listSelector) ?: device.findObject(By.scrollable(true))
requireNotNull(list) { "Feed list not found!" }
list.setGestureMargin(device.displayWidth / 5)
list.fling(Direction.UP)
device.waitForIdle()
}
}

@Test
fun navigateSnackDetailFrameTiming() = benchmarkRule.measureRepeated(
packageName = targetPackageName,
metrics = listOf(FrameTimingMetric()),
compilationMode = CompilationMode.DEFAULT,
startupMode = StartupMode.COLD,
iterations = 10,
setupBlock = {
pressHome()
val context = InstrumentationRegistry.getInstrumentation().context
val intent = context.packageManager.getLaunchIntentForPackage(packageName)!!
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
device.wait(Until.hasObject(By.pkg(packageName).depth(0)), 15000)
device.wait(Until.hasObject(By.res("feed_list")), 15000)
device.wait(Until.hasObject(By.res("highlight_snack_item")), 10000)
device.waitForIdle()
},
) {
val listSelector = By.res("feed_list")

// Find snack item to click and trigger shared element transition
val snackItem = device.wait(Until.findObject(By.text("Cupcake")), 10000)
?: device.wait(Until.findObject(By.text("Donut")), 5000)
?: device.wait(Until.findObject(By.res("highlight_snack_item")), 5000)
?: device.findObject(By.res("snack_item"))
?: device.findObject(By.clickable(true))
requireNotNull(snackItem) { "Snack item not found on feed!" }

// Click snack to navigate into detail with shared element transition
snackItem.click()

// Wait for detail screen to appear
val detailLoaded = device.wait(Until.hasObject(By.desc("Back")), 10000) ||
device.wait(Until.hasObject(By.text("Details")), 5000) ||
device.wait(Until.hasObject(By.textContains("CART")), 5000)
check(detailLoaded) { "Detail screen not loaded!" }
device.waitForIdle()

// Return to feed with reverse shared transition
device.pressBack()
device.wait(Until.hasObject(listSelector), 10000)
device.waitForIdle()
}
}
1 change: 1 addition & 0 deletions Jetsnack/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ plugins {
alias(libs.plugins.gradle.versions)
alias(libs.plugins.version.catalog.update)
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.compose) apply false
alias(libs.plugins.spotless) apply false
Expand Down
2 changes: 2 additions & 0 deletions Jetsnack/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ android-material3 = "1.14.0"
androidGradlePlugin = "9.3.1"
androidx-activity-compose = "1.13.0"
androidx-appcompat = "1.8.0"
androidx-benchmark = "1.3.3"
androidx-compose-bom = "2026.08.00"
androidx-core-splashscreen = "1.2.0"
androidx-corektx = "1.19.0"
Expand Down Expand Up @@ -110,6 +111,7 @@ androidx-test-ext-truth = { module = "androidx.test.ext:truth", version.ref = "a
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" }
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androix-test-uiautomator" }
androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" }
androidx-tv-foundation = { module = "androidx.tv:tv-foundation", version.ref = "androidx-tv-foundation" }
androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "androidx-tv-material" }
androidx-wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "androidx-wear-compose" }
Expand Down
1 change: 1 addition & 0 deletions Jetsnack/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ dependencyResolutionManagement {
}
rootProject.name = "Jetsnack"
include(":app")
include(":benchmark")
Loading