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

Вы можете столкнуться с распространенными ошибками 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 : при компоновке больше не требуется перекомпоновать весь Box .

Это хорошее улучшение, но вы можете добиться большего! Вам следует с подозрением относиться к тому, что вы вызываете рекомпозицию только для того, чтобы переразметить или перерисовать Composable. В этом случае все, что вы делаете, — это меняете смещение составного 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 { mutableStateOf(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 перекомпонует этот Composable, видит прочитанное состояние, которое устарело, и поэтому планирует другое рекомпозиция.

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

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

{% дословно %} {% дословно %} {% дословно %} {% дословно %}