Следуйте рекомендациям

Вы можете столкнуться с распространенными ошибками в Compose. Эти ошибки могут привести к тому, что ваш код будет казаться достаточно работоспособным, но на самом деле ухудшит производительность пользовательского интерфейса. Следуйте лучшим практикам для оптимизации вашего приложения в Compose.

remember это поможет свести к минимуму дорогостоящие вычисления.

Композитные функции могут выполняться очень часто , вплоть до каждого кадра анимации. Поэтому следует свести к минимуму вычисления в теле композитивной функции.

Важный приём — сохранение результатов вычислений с помощью remember . Таким образом, вычисление выполняется один раз, и вы можете получить доступ к результатам, когда они понадобятся.

Например, вот код, который отображает отсортированный список имен, но сортировка выполняется очень ресурсоемким способом:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

При каждом пересоздании ContactsList ) весь список контактов сортируется заново, даже если сам список не изменился. Если пользователь прокручивает список, компонент (Composable) пересоздается при появлении новой строки.

Для решения этой проблемы отсортируйте список вне LazyColumn и сохраните отсортированный список с помощью remember :

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

Теперь список сортируется один раз, при первом создании ContactList . Если контакты или компаратор изменяются, отсортированный список генерируется заново. В противном случае, создаваемый объект может продолжать использовать кэшированный отсортированный список.

Используйте клавиши отложенной раскладки.

Ленивая компоновка эффективно использует элементы повторно, перегенерируя или перекомпоновывая их только тогда, когда это необходимо. Однако вы можете помочь оптимизировать ленивую компоновку для перекомпоновки.

Предположим, что действие пользователя приводит к перемещению элемента в списке. Например, предположим, вы отображаете список заметок, отсортированных по времени изменения, причем самая недавно измененная заметка находится сверху.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

Однако в этом коде есть проблема. Предположим, изменена нижняя нота. Теперь это самая недавно измененная нота, поэтому она перемещается в начало списка, а все остальные ноты опускаются на одну позицию вниз.

Без вашей помощи Compose не понимает, что неизмененные элементы просто перемещаются в списке. Вместо этого Compose считает, что старый «элемент 2» был удален, а для элементов 3, 4 и так далее были созданы новые. В результате Compose пересобирает каждый элемент в списке, хотя на самом деле изменился только один из них.

Решение здесь заключается в предоставлении ключей для элементов . Предоставление стабильного ключа для каждого элемента позволяет Compose избежать ненужных перекомпоновок. В этом случае Compose может определить, что элемент, находящийся сейчас на позиции 3, — это тот же самый элемент, который раньше находился на позиции 2. Поскольку данные этого элемента не изменились, Compose не нужно его перекомпоновывать.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

Используйте derivedStateOf для ограничения рекомпозиций.

Один из рисков использования состояния в композициях заключается в том, что при быстром изменении состояния ваш пользовательский интерфейс может перестраиваться чаще, чем это необходимо. Например, предположим, вы отображаете прокручиваемый список. Вы проверяете состояние списка, чтобы определить, какой элемент является первым видимым элементом в списке:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Проблема в том, что если пользователь прокручивает список, listState постоянно меняется по мере того, как пользователь перемещает палец. Это означает, что список постоянно перестраивается. Однако на самом деле перестраивать его нужно не так часто — это происходит только тогда, когда внизу появляется новый элемент. Таким образом, это приводит к большим дополнительным вычислениям, что ухудшает производительность пользовательского интерфейса.

Решение заключается в использовании производного состояния . Производное состояние позволяет указать Compose, какие изменения состояния действительно должны запускать перекомпозицию. В данном случае укажите, что вас интересует момент изменения первого видимого элемента. Когда значение этого состояния изменяется, пользовательский интерфейс должен перекомпоноваться, но если пользователь еще не прокрутил достаточно, чтобы новый элемент оказался вверху, перекомпоновка не требуется.

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Откладывайте чтение как можно дольше.

Когда обнаружена проблема с производительностью, отложенное чтение состояния может помочь. Отложенное чтение состояния гарантирует, что Compose повторно выполнит минимально возможный код при рекомпозиции. Например, если состояние вашего пользовательского интерфейса находится высоко в дереве компонуемых объектов, и вы считываете это состояние в дочернем компонуемом объекте, вы можете обернуть чтение состояния в лямбда-функцию. Это позволит считыванию происходить только тогда, когда это действительно необходимо. Для справки см. реализацию в примере приложения Jetsnack . Jetsnack реализует эффект, похожий на сворачивающуюся панель инструментов, на экране сведений. Чтобы понять, почему этот метод работает, см. статью в блоге Jetpack Compose: Debugging Recomposition .

Для достижения этого эффекта компоненту Title требуется смещение прокрутки, чтобы он мог сместиться сам с помощью Modifier . Вот упрощенная версия кода Jetsnack до оптимизации:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Когда состояние прокрутки изменяется, Compose аннулирует ближайшую область рекомпозиции родительского элемента. В данном случае ближайшей областью является компонуемый объект SnackDetail . Обратите внимание, что Box — это встроенная функция, и поэтому она не является областью рекомпозиции. Таким образом, Compose перекомпоновывает SnackDetail и любые компонуемые объекты внутри SnackDetail . Если вы измените свой код так, чтобы он считывал только то состояние, в котором вы фактически используете объект, то вы сможете уменьшить количество элементов, которые необходимо перекомпоновать.

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

Параметр прокрутки теперь имеет тип лямбда. Это означает, что Title по-прежнему может ссылаться на поднятое состояние, но значение считывается только внутри Title , где оно действительно необходимо. В результате, при изменении значения прокрутки ближайшая область рекомпозиции теперь является компонуемым элементом Title — Compose больше не нужно перекомпоновывать весь Box .

Это хорошее улучшение, но можно сделать лучше! Следует насторожиться, если вы вызываете перекомпозицию только для того, чтобы изменить расположение или перерисовать составной элемент. В данном случае вы просто изменяете смещение составного Title «Заголовок», что можно было бы сделать на этапе компоновки.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

Ранее в коде использовался Modifier.offset(x: Dp, y: Dp) , который принимает смещение в качестве параметра. Переключившись на лямбда-версию модификатора , вы можете гарантировать, что функция будет считывать состояние прокрутки на этапе компоновки. В результате, при изменении состояния прокрутки Compose сможет полностью пропустить этап композиции и перейти непосредственно к этапу компоновки. При передаче часто изменяющихся переменных состояния в модификаторы следует по возможности использовать лямбда-версии модификаторов.

Вот ещё один пример такого подхода. Этот код ещё не оптимизирован:

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

В данном случае цвет фона блока быстро переключается между двумя цветами. Таким образом, это состояние меняется очень часто. Затем компонент считывает это состояние в модификаторе фона. В результате блок приходится перекомпоновывать на каждом кадре, поскольку цвет меняется на каждом кадре.

Для улучшения этого можно использовать модификатор на основе лямбда-функций — в данном случае, drawBehind . Это означает, что состояние цвета считывается только на этапе отрисовки. В результате Compose может полностью пропускать этапы композиции и компоновки — при изменении цвета Compose сразу переходит к этапу отрисовки.

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

Избегайте обратной записи.

В основе Compose лежит предположение, что вы никогда не будете записывать данные в состояние, которое уже было прочитано . Когда вы это делаете, это называется обратной записью , и это может привести к бесконечной перекомпозиции каждого кадра.

Следующий пример демонстрирует подобную ошибку.

@Composable
fun BadComposable() {
    var count by remember { mutableIntStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

Этот код обновляет счетчик в конце составного объекта после его считывания на предыдущей строке. Если вы запустите этот код, вы увидите, что после нажатия кнопки, которая вызывает перекомпозицию, счетчик быстро увеличивается в бесконечном цикле, поскольку Compose перекомпоновывает этот составной объект, обнаруживает, что считанное состояние устарело, и поэтому планирует следующую перекомпозицию.

Можно полностью избежать обратной записи, никогда не записывая данные в состояние в Composition. По возможности всегда записывайте данные в состояние в ответ на событие и в лямбда-функции, как в предыдущем примере onClick .

Дополнительные ресурсы

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}