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
8 changes: 8 additions & 0 deletions JetNews/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ android {
"proguard-rules.pro",
)
}

create("benchmark") {
initWith(getByName("debug"))
isDebuggable = false
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
matchingFallbacks.add("debug")
}
}
kotlin {
compilerOptions {
Expand Down
23 changes: 23 additions & 0 deletions JetNews/app/src/benchmark/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
~
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required for macrobenchmark to write trace files on some API levels -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<profileable android:shell="true" />
</application>
</manifest>
35,120 changes: 35,120 additions & 0 deletions JetNews/app/src/main/baseline-prof.txt

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions JetNews/app/src/main/java/com/example/jetnews/model/Post.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package com.example.jetnews.model

import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable

@Immutable
data class Post(
val id: String,
val title: String,
Expand All @@ -30,14 +32,19 @@ data class Post(
@DrawableRes val imageThumbId: Int,
)

@Immutable
data class Metadata(val author: PostAuthor, val date: String, val readTimeMinutes: Int)

@Immutable
data class PostAuthor(val name: String, val url: String? = null)

@Immutable
data class Publication(val name: String, val logoUrl: String)

@Immutable
data class Paragraph(val type: ParagraphType, val text: String, val markups: List<Markup> = emptyList())

@Immutable
data class Markup(val type: MarkupType, val start: Int, val end: Int, val href: String? = null)

enum class MarkupType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
Expand Down Expand Up @@ -168,17 +170,33 @@ fun HomeScreen(
// UiState of the HomeScreen
val uiState by homeViewModel.uiState.collectAsStateWithLifecycle()

val onToggleFavorite = remember(homeViewModel) {
{ postId: String -> homeViewModel.toggleFavourite(postId) }
}
val onSelectPost = remember(navigateToPost) {
{ postId: String -> navigateToPost(postId) }
}
val onRefreshPosts = remember(homeViewModel) {
{ homeViewModel.refreshPosts() }
}
val onErrorDismiss = remember(homeViewModel) {
{ errorId: Long -> homeViewModel.errorShown(errorId) }
}
val onSearchInputChanged = remember(homeViewModel) {
{ searchInput: String -> homeViewModel.onSearchInputChanged(searchInput) }
}

HomeFeedScreen(
uiState = uiState,
showTopAppBar = !isExpandedScreen,
onToggleFavorite = { homeViewModel.toggleFavourite(it) },
onSelectPost = { navigateToPost(it) },
onRefreshPosts = { homeViewModel.refreshPosts() },
onErrorDismiss = { homeViewModel.errorShown(it) },
onToggleFavorite = onToggleFavorite,
onSelectPost = onSelectPost,
onRefreshPosts = onRefreshPosts,
onErrorDismiss = onErrorDismiss,
openDrawer = openDrawer,
homeListLazyListState = rememberLazyListState(),
snackbarHostState = snackbarHostState,
onSearchInputChanged = { homeViewModel.onSearchInputChanged(it) },
onSearchInputChanged = onSearchInputChanged,
searchInput = uiState.searchInput,
)
}
Expand Down Expand Up @@ -422,13 +440,14 @@ private fun PostList(
}
item { PostListTopSection(postsFeed.highlightedPost, onPostTapped) }
if (postsFeed.recommendedPosts.isNotEmpty()) {
item {
PostListSimpleSection(
postsFeed.recommendedPosts,
onPostTapped,
favorites,
onToggleFavorite,
items(postsFeed.recommendedPosts) { post ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In LazyColumn, it is highly recommended to specify a unique key for items. This helps Jetpack Compose maintain the scroll position and state of items when the list changes (e.g., when a post is favorited or the list is refreshed), preventing unnecessary recompositions and improving scroll performance.

Suggested change
items(postsFeed.recommendedPosts) { post ->
items(postsFeed.recommendedPosts, key = { it.id }) { post ->

PostCardSimple(
post = post,
navigateToPost = onPostTapped,
isFavorite = favorites.contains(post.id),
onToggleFavorite = { onToggleFavorite(post.id) }
)
PostListDivider()
}
}
if (postsFeed.popularPosts.isNotEmpty() && !showExpandedSearch) {
Expand All @@ -439,7 +458,10 @@ private fun PostList(
}
}
if (postsFeed.recentPosts.isNotEmpty()) {
item { PostListHistorySection(postsFeed.recentPosts, onPostTapped) }
items(postsFeed.recentPosts) { post ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Specify a unique key for the recent posts items to optimize scroll performance and prevent unnecessary recompositions when the list updates.

Suggested change
items(postsFeed.recentPosts) { post ->
items(postsFeed.recentPosts, key = { it.id }) { post ->

PostCardHistory(post, onPostTapped)
PostListDivider()
}
}
}
}
Expand Down Expand Up @@ -478,33 +500,7 @@ private fun PostListTopSection(post: Post, navigateToPost: (String) -> Unit) {
PostListDivider()
}

/**
* Full-width list items for [PostList]
*
* @param posts (state) to display
* @param navigateToPost (event) request navigation to Post screen
* @param favorites (state) a set of favorite posts
* @param onToggleFavorite (event) request that this post toggle its favorite state
*/
@Composable
private fun PostListSimpleSection(
posts: List<Post>,
navigateToPost: (String) -> Unit,
favorites: Set<String>,
onToggleFavorite: (String) -> Unit,
) {
Column {
posts.forEach { post ->
PostCardSimple(
post = post,
navigateToPost = navigateToPost,
isFavorite = favorites.contains(post.id),
onToggleFavorite = { onToggleFavorite(post.id) },
)
PostListDivider()
}
}
}


/**
* Horizontal scrolling cards for [PostList]
Expand All @@ -520,14 +516,13 @@ private fun PostListPopularSection(posts: List<Post>, navigateToPost: (String) -
text = stringResource(id = R.string.home_popular_section_title),
style = MaterialTheme.typography.titleLarge,
)
Row(
LazyRow(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.height(IntrinsicSize.Max)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
for (post in posts) {
items(posts) { post ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Specify a unique key for the popular posts items in the LazyRow to optimize horizontal scrolling performance and prevent unnecessary recompositions.

Suggested change
items(posts) { post ->
items(posts, key = { it.id }) { post ->

PostCardPopular(
post,
navigateToPost,
Expand All @@ -539,21 +534,7 @@ private fun PostListPopularSection(posts: List<Post>, navigateToPost: (String) -
}
}

/**
* Full-width list items that display "based on your history" for [PostList]
*
* @param posts (state) to display
* @param navigateToPost (event) request navigation to Post screen
*/
@Composable
private fun PostListHistorySection(posts: List<Post>, navigateToPost: (String) -> Unit) {
Column {
posts.forEach { post ->
PostCardHistory(post, navigateToPost)
PostListDivider()
}
}
}


/**
* Full-width divider with padding for [PostList]
Expand Down
48 changes: 48 additions & 0 deletions JetNews/benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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
*
* 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.
*/

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

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

defaultConfig {
minSdk = 23
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")
}
}

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

dependencies {
implementation(libs.androidx.test.rules)
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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
*
* 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.jetnews.benchmark

import android.content.Intent
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
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.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

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

private val targetPackageName = "com.example.jetnews"

@Test
fun startupCold() = benchmarkRule.measureRepeated(
packageName = targetPackageName,
metrics = listOf(StartupTimingMetric()),
compilationMode = CompilationMode.None(),
startupMode = StartupMode.COLD,
iterations = 5
) {
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)
}

@Test
fun startupHot() = benchmarkRule.measureRepeated(
packageName = targetPackageName,
metrics = listOf(StartupTimingMetric()),
compilationMode = CompilationMode.None(),
startupMode = StartupMode.HOT,
iterations = 5
) {
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)
}
}
Loading
Loading