Irbisa · Шпаргалка
Производительность и стабильность Compose
Стабильность, пропуск рекомпозиции, фазы, ключи Lazy, Modifier.Node, метрики компилятора
Что вы делаете по умолчанию, ещё до профайлера, чтобы экран на Compose работал быстро?
Почему список на Compose дёргается в debug-сборке и плавно скроллится в release, и какую сборку профилировать вместо debug?
// app/build.gradle.kts: release code, debug signing, profileableandroid { buildTypes { create("profiling") { initWith(getByName("release")) // R8 and release flags signingConfig = signingConfigs.getByName("debug")…Что не так с этим composable и почему экран держит процессор занятым, хотя на нём ничего не меняется?
@Composablefun Feed(posts: List<Post>) { var renders by remember { mutableIntStateOf(0) } Text("Rendered $renders times") LazyColumn { items(posts, key = { it.id }) { PostRow(it) } } renders++}Найдите баг: пока баннер сети выезжает, Layout Inspector показывает рекомпозицию
ChatScreenна каждом кадре анимации.@Composablefun ChatScreen(messages: List<Message>, showBanner: Boolean) { val bannerOffset by animateDpAsState( targetValue = if (showBanner) 0.dp else (-64).dp, label = "banner", ) Box(Modifier.fillMaxSize()) { MessageList(messages) NetworkBanner(Modifier.offset(y = bannerOffset).height(64.dp)) }}Найдите баг: список контактов не меняется, когда пользователь вводит текст в поле поиска.
@Composablefun ContactList(contacts: List<Contact>, query: String) { // filtering is expensive, so cache it val visible by remember { derivedStateOf { contacts.filter { it.name.contains(query, ignoreCase = true) } } } LazyColumn { items(visible, key = { it.id }) { ContactRow(it) } }}Что именно контролируют
keyиcontentTypeвLazyColumnи что идёт не так при быстром скролле, еслиcontentTypeне задан?sealed interface FeedItem { data class Header(val id: String, val title: String) : FeedItem data class Photo(val id: String, val url: String) : FeedItem data class Comment(val id: String, val text: String) : FeedItem}…Layout Inspector показывает 300 рекомпозиций у шапки, содержимое которой никогда не меняется. Как читать это число и дойти от него до чтения состояния?
// A count readable without the inspector, in any build:@Composablefun LogRecompositions(tag: String) { val count = remember { IntArray(1) } SideEffect { Log.d("recompose", "$tag = ${++count[0]}") }}…Экран ленты перекомпонуется гораздо чаще, чем должен. Как заставить сам Compose-компилятор назвать виновный параметр?
// build.gradle.ktscomposeCompiler { // ./gradlew assembleRelease -PcomposeReports if (project.findProperty("composeReports") != null) { reportsDestination = layout.buildDirectory.dir("compose_reports") metricsDestination = layout.buildDirectory.dir("compose_metrics")…Что делает тип стабильным в Compose и что на самом деле обещают
@Stableи@Immutable?// <module>-classes.txt from the compiler report:// unstable class Article {// stable val id: String// unstable val tags: List<Tag> <- this field makes the class unstable// }…Strong skipping включён по умолчанию с Compose-компилятора из Kotlin 2.0.20. Что он изменил и что по-прежнему не чинит?
data class Article(val id: String, val tags: List<Tag>) // unstable param @Composablefun Feed(articles: List<Article>) { LazyColumn { items(articles, key = { it.id }) { article ->…progressменяется каждый кадр. ПочемуControlsтоже рекомпозируется каждый кадр и как это исправить, не получив устаревший колбэк?@Composablefun Player(progress: Float, onSeek: (Float) -> Unit) { Text("At ${progress.toInt()} s") Controls(onSkip = { onSeek(progress + 10f) })}Что плагин Compose-компилятора делает с
@Composable-функцией и что такое$composer,$changed, группы и slot table?Когда
wideпереключается (складной телефон раскладывается, меняется размер окна),Columnменяется наRow, и видеоплеер начинает с нуля. Как перенести поддерево к новому родителю, не потеряв состояние?// Two call sites: when `wide` flips, the player is disposed and rebuilt@Composablefun ScreenBroken(wide: Boolean, url: String) { if (wide) Row { Player(url); Details() } else Column { Player(url); Details() }}…Ваш собственный
Modifier.pulsingBorder()написан черезcomposed { }. Чего это стоит и что изменится, если переписать его наModifier.Node?// composed: a composition group per usage, no equals; the read of `alpha`// in the factory body invalidates compositionfun Modifier.pulsingBorderSlow(color: Color) = composed { val alpha by rememberInfiniteTransition(label = "pulse") .animateFloat(0f, 1f, infiniteRepeatable(tween(800)), label = "alpha") Modifier.border(2.dp, color.copy(alpha = alpha)) // read in the factory body…Кто-то поставил
BoxWithConstraintsвнутрь каждого элементаLazyColumn, и скролл стал хуже. Чего на самом деле стоит субкомпозиция и что надо было использовать?// A SubcomposeLayout per row: composition moves into the measure passLazyColumn { items(posts, key = { it.id }) { post -> BoxWithConstraints { if (maxWidth > 600.dp) WideRow(post) else NarrowRow(post) }…Отчёт компилятора говорит, что строка ленты не skippable из-за модели из стороннего SDK. Какие есть варианты и чего стоит каждый?
// build.gradle.ktscomposeCompiler { stabilityConfigurationFiles.add( rootProject.layout.projectDirectory.file("compose_stability.conf") ) reportsDestination = layout.buildDirectory.dir("compose_reports")…Свежеустановленная сборка лагает на первом скролле списка на Compose и идеально гладкая на всех последующих. Что делает рантайм и что это реально чинит?
// Generate the profile for THIS journey, not just for cold start@RunWith(AndroidJUnit4::class)class FeedBaselineProfile { @get:Rule val rule = BaselineProfileRule() @Test fun feedScroll() = rule.collect(packageName = "com.example.app") {…Счётчики рекомпозиций чистые, но кадры всё равно опаздывают, а время висит на RenderThread. Где Compose-интерфейс обычно жжёт время GPU?
// An offscreen buffer for a fade that did not need oneBox(Modifier.alpha(fade)) { Row { Icon(icon, null); Text(label) } } // Children do not overlap -> modulate alpha per draw op, no offscreen bufferBox( Modifier.graphicsLayer {…Как не дать регрессии производительности Compose попасть в main и что делает число со скролла достаточно стабильным, чтобы валить на нём pull request?