Перейти к содержимому
Источник

Про ключи в Lazy лэйаутах Compose

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

LazyColumn {
  items(items, key = { it.id }) {
    ListItem(it.text)
  }
}

Когда мы хотим сюда добавить какой-то хедер возникает абсолютно естественное желание по аналогии написать вот так:

LazyColumn {
  item(key = { "header" }) { Header() }
  items(items, key = { item -> item.id }) {
    ListItem(it.text)
  }
}

Собирается, запускается, работает. И потом крашится в проде. 😮

Наблюдаем за прелестями композного API. Вот функция items (упрощённая):

fun <T> LazyListScope.items(
  items: List<T>,
  key: ((T) -> Any)? = null,
  contentType: (T) -> Any? = { null },
  itemContent: @Composable LazyItemScope.(T) -> Unit
)

А вот один item:

fun item(
  key: Any? = null,
  contentType: Any? = null,
  content: @Composable LazyItemScope.() -> Unit
)

И в том и в другом случае мы можем передать в key лямбду, потому что у ребят из гугла тут Any? в параметрах. Почему бы нет. И только в комментарии рядом с функцией осторожно написано, что key под капотом на андроиде будет сохраняться в Bundle, поэтому он должен быть или примитивным, или строкой, или Parcelable. А в случае с items - всё тоже самое актуально для key().

Соответственно тут нужно быть офигенно осторожным и помнить что писать надо так:

LazyColumn {
  item(key = "header") { Header() }
  items(items, key = { item -> item.id }) {
    ListItem(it.text)
  }
}

Если бы я писал функцию item, то я бы перегрузил её для всех сохраняемых в бандл типов, наверно. Или вообще явную Parcelable обёртку для ключа сделал, её же и в лямбде для items возвращал.

Это вот тот самый кейс за который вас по рукам на код-ревью бьют за Any? где не нужно. Подозреваю, что они таким лайфхаком решали проблему зависимости от Android SDK в foundation, но от этого не проще, честно говоря.