image

Я в своем время наткнулся на довольно интересное поведение composable функций в списках, тогда мне помогла статья с медиума и чтение книги по compose internals, ссылки на них я приложу в конце статьи. Решил поделиться, возможно кому-то это поможет)

Отображение списков распространенный кейс для большинства приложений и зачастую эти списки динамические, в них меняется контент, позиции элементов и т.д. Есть довольно быстрый способ оптимизировать ваш список в compose.
Но для начала рассмотрим, что такое «место вызова» (call site), composable функции.

Composable


Каждая функция, помеченная @Composable аннотацией будет иметь дополнительную информацию для kotlin компилятора о том, что она эмитит UI. Когда компилятор проходит по этой функции он добавляет информацию о месте ее вызова, эти данные являются уникальным идентификатором для composable функции.

Что такое место вызова (call site)


Информация о месте вызова необходима для переиспользования во время стадии recomposition дерева composable функций.

@Composable
fun Example(
    firstText: String,
    secondText: String
) {
    Column() {
        Text(firstText) // место вызова для composable с firstText
        Text(secondText) // место вызова для composable с secondText
    }
}


Так как обе composable функции вызываются в разных местах, то у каждой их них есть свой уникальный идентификатор места вызова.
Но давайте посмотрим на такой пример:

@Composable
fun Loop() {
    Column {
        for (num in 1..100) {
            Text(num.toString())
        }
    }
}


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

Но у нас проблемы по коням Андрюха, у нас криминал.
Давайте представим, что список модифицируемый. Что произойдет если новый элемент добавится в начало списка? Место вызова каждой созданной composable функции зависело от позиции элемента в списке, так как все позиции поменялись теперь ни одно произведенное вычисление composable функций не может быть переиспользовано, хотя контент не менялся. Такое поведение стоит поправить.

Оптимизация через Key


Давайте предоставлять уникальную информацию для места вызова вместо компилятора

@Composable
fun Loop() {
    Column {
        for (someObj in ourList) {
            key(someObj.key) {
                Text(someObj)
            }
        }
    }
}


Теперь место вызова еще имеет информацию которую мы предоставляем через key. В таком кейсе при добавлении нового элемента в начало списка новая composable функция инициализируется только для этого айтема, все остальные заэмиченные composable функции будут переиспользованы.
Такую оптимизацию мы также можем добавить для LazyColumn и LazyRow:

@Composable
fun Loop() {
    LazyColumn(ourList, {someObject -> someObject.id}){
        Text(someObj)
    }
}

Список вдоховителей:


Статья, освещающая, данную тему на английском
Полезная книга по внутренностям compose

Комментарии (1)


  1. EvgenWithYou
    17.01.2022 17:03

    Хорошая статья, автор молодец