Jetpack Compose: изменяемый список в LazyColumn
Как правильно отображать изменяемый список заказов в Jetpack Compose с LazyColumn. Обновления элементов, ViewModel, StateFlow, mutableStateListOf, key. Примеры кода, плюсы и минусы подходов для фоновых обновлений.
Как правильно в Jetpack Compose отображать изменяемый список заказов и отслеживать обновления отдельных элементов?
Контекст:
- Загружаю по API список заказов как обычные POJO и отображаю их в LazyColumn.
- Список может меняться (добавление/удаление/изменение порядка), и поля отдельных заказов тоже могут меняться (например, фоновая служба обновляет status).
Вопросы:
- Какой подход считается правильным, чтобы Compose автоматически перерисовывал список и элементы при изменениях? Достаточно ли использовать mutableListOf, или лучше mutableStateListOf / SnapshotStateList / Snapshot state?
- При изменении полей заказа: стоит ли делать поля модели observable (например, var status by mutableStateOf(…)) внутри POJO, или лучше использовать неизменяемые data class и при обновлении заменять объект в списке (copy + замена в состоянии)?
- Нужен ли key в LazyColumn в таких сценариях и как правильно его использовать?
- Какие архитектурные практики/паттерны вы порекомендуете (ViewModel + StateFlow/LiveData, хранить snapshot state в ViewModel, делегаты mutableStateOf и т. п.) для приложения, где список и отдельные элементы могут обновляться из фоновой службы?
Буду признателен за короткие примеры кода и объяснение плюсов/минусов каждого подхода.
В Jetpack Compose для корректного отображения изменяемого списка заказов в LazyColumn лучше держать состояние в ViewModel и использовать либо immutable data class + MutableStateFlow/StateFlow (обновлять через copy() и замену в списке), либо Compose‑специфичный mutableStateListOf/SnapshotStateList. Обычный mutableListOf не вызывает recomposition; делать поля модели observable через mutableStateOf можно, но это ломает иммутабельность и усложняет архитектуру. Обязательно указывайте стабильный key в LazyColumn, если элементы добавляются/удаляются или меняют порядок.
Содержание
- Как Compose отслеживает изменения
- mutableListOf vs mutableStateListOf / SnapshotStateList
- Immutable data class vs observable поля (mutableStateOf)
- Key в LazyColumn: зачем и как использовать
- Архитектурные практики: ViewModel, StateFlow, Repository
- Короткие примеры кода
- Плюсы и минусы подходов
- Источники
- Заключение
Как Compose отслеживает изменения (Jetpack Compose, LazyColumn)
Compose перерисовывает только те composable, которые читают «наблюдаемые» состояния. К таким состояниям относятся mutableStateOf, SnapshotStateList (mutableStateListOf) и состояние, полученное через collectAsState() из StateFlow/LiveData. Обычный mutableListOf — это просто коллекция Kotlin: изменение её содержимого не является snapshot‑событием и не вызовет recomposition.
Почему это важно? Потому что если ваш UI читает не‑наблюдаемую коллекцию, он не узнает об обновлениях. С другой стороны, StateFlow/MutableStateFlow — корутино‑дружелюбный инструмент для потоков данных и безопаснее при обновлениях из background‑потоков (и удобнее тестировать). Подробнее про рекомендуемый подход с ViewModel и State смотрите в официальном codelab Android Developers.
Небольшая заметка про потоки и потоки снимков: snapshot‑состояния ориентированы на UI‑поток; если обновления приходят из фоновой службы, безопаснее передавать их через Flow/StateFlow и только затем обновлять snapshot‑состояние на главном потоке.
mutableListOf vs mutableStateListOf / SnapshotStateList
- mutableListOf: не наблюдаем, Compose не реагирует на
add/remove/set. - mutableStateListOf / SnapshotStateList: операции над списком наблюдаемы — Compose перерисует те composable, которые читают этот список или его элементы.
- MutableStateFlow<List
>: тоже наблюдаем, но вы обычно заменяете весь список ( value = newListилиupdate { ... }) — что хорошо для иммутабельного подхода.
Пример, который НЕ сработает (не будет recomposition):
// ViewModel
val orders = mutableListOf<Order>()
fun setOrders(new: List<Order>) {
orders.clear()
orders.addAll(new)
}
// Composable — чтение обычного List: Compose не увидит изменения
val current = viewModel.orders
LazyColumn {
items(current) { order -> /* ... */ }
}
Рабочий вариант с mutableStateListOf:
class OrdersViewModel : ViewModel() {
private val _orders = mutableStateListOf<Order>()
val orders: List<Order> get() = _orders
fun setOrders(new: List<Order>) {
_orders.clear()
_orders.addAll(new) // Compose увидит изменения
}
fun updateStatus(id: Long, newStatus: Status) {
val idx = _orders.indexOfFirst { it.id == id }
if (idx != -1) _orders[idx] = _orders[idx].copy(status = newStatus)
}
}
Альтернатива с MutableStateFlow<List<Order>> (рекомендуется для архитектуры):
private val _orders = MutableStateFlow<List<Order>>(emptyList())
val orders: StateFlow<List<Order>> = _orders.asStateFlow()
fun updateStatus(id: Long, newStatus: Status) {
_orders.update { list -> list.map { if (it.id == id) it.copy(status = newStatus) else it } }
}
(Подробнее о snapshot‑state vs flow — есть полезные разъяснения на droidcon.)
Immutable data class vs observable поля (mutableStateOf)
Есть два распространённых подхода для модели элемента:
- Immutable data class + замена объекта в списке (рекомендую)
- data class Order(val id: Long, val status: Status, …)
- При изменении:
orders = orders.map { if (it.id==id) it.copy(status=new) else it }(для StateFlow) или_stateList[idx] = _stateList[idx].copy(...)(для SnapshotStateList). - Плюсы: простая семантика, безопасность потоков (при использовании Flow), легкость тестирования, предсказуемость.
- Минусы: при очень частых изменениях возможны аллокации новых списков/объектов (обычно не проблема).
- Observable поля внутри модели (UI‑модель с
var status by mutableStateOf(...))
- class OrderUi(val id: Long, status: Status)
- Плюсы: можно менять поле напрямую, и item‑Composable, читающий это поле, перерисуется.
- Минусы: нарушается иммутабельность; сложнее сериализовать/передать модель между слоями; обновления должны выполняться в snapshot‑контексте (обычно на главном потоке); тестировать сложнее.
В большинстве продакшен‑кейсов я советую immutable data class + StateFlow (или SnapshotStateList в простых UI‑ориентированных ситуациях). Это совпадает с рекомендациями из официального codelab Android Developers.
Key в LazyColumn: зачем и как использовать
Если элементы могут добавляться/удаляться/переставляться, явно указывайте key, чтобы Compose связывал состояние элементов по стабильному идентификатору, а не по индексу.
Пример:
LazyColumn {
items(
items = orders,
key = { it.id } // использовать уникальный и стабильный id
) { order ->
OrderRow(order = order)
}
}
Без key при перестановке элементов локальное состояние item’а (например, текст в TextField, анимация, фокус) может «переехать» к другому объекту — потому что сопоставление происходит по позиции. Ключ решает эту проблему.
Архитектурные практики: ViewModel, StateFlow, Repository
Рекомендованная схема: Repository (источник API/DB/фоновой службы) → ViewModel → UI (Compose). Варианты:
- Repository предоставляет Flow/StateFlow (например, Room DAO возвращает
Flow<List<OrderEntity>>), ViewModel собирает и либо напрямую прокидываетStateFlow<List<Order>>, либо мэппит в UI‑модели. - ViewModel хранит единую точку истины:
MutableStateFlow<List<Order>>(илиmutableStateListOfдля UI‑связанного списка). UI используетcollectAsState()или читает snapshot‑список. - Для фоновых обновлений (сервис, web socket) безопаснее публиковать изменения в Flow/StateFlow — они потокобезопасны. Затем переводите в snapshot‑состояние на главном потоке, если нужно.
Пример потока обновлений из репозитория:
init {
viewModelScope.launch {
repository.ordersFlow.collect { list ->
_orders.value = list // если _orders — MutableStateFlow
// или: withContext(Dispatchers.Main) { _snapshotList.clear(); _snapshotList.addAll(list) }
}
}
}
Когда выбирать что:
- Для чистой архитектуры, тестируемости и фоновых обновлений —
StateFlow+ immutable модели. - Для простых UI‑only случаев, где элементы часто меняются in‑place и не нужны тесты/слой repository —
mutableStateListOfможно держать в ViewModel.
Короткие примеры кода
- StateFlow + immutable data class (рекомендуемый):
data class Order(val id: Long, val status: Status)
class OrdersViewModel : ViewModel() {
private val _orders = MutableStateFlow<List<Order>>(emptyList())
val orders: StateFlow<List<Order>> = _orders.asStateFlow()
fun setOrdersFromApi(list: List<Order>) { _orders.value = list }
fun updateStatus(id: Long, newStatus: Status) {
_orders.update { cur -> cur.map { if (it.id == id) it.copy(status = newStatus) else it } }
}
}
@Composable
fun OrdersScreen(vm: OrdersViewModel = viewModel()) {
val orders by vm.orders.collectAsState()
LazyColumn {
items(items = orders, key = { it.id }) { order ->
OrderRow(order)
}
}
}
- Snapshot state list (
mutableStateListOf):
class OrdersViewModel : ViewModel() {
private val _orders = mutableStateListOf<Order>()
val orders: List<Order> get() = _orders
fun setOrders(new: List<Order>) {
_orders.clear()
_orders.addAll(new)
}
fun updateStatus(id: Long, newStatus: Status) {
val idx = _orders.indexOfFirst { it.id == id }
if (idx != -1) _orders[idx] = _orders[idx].copy(status = newStatus)
}
}
@Composable
fun OrdersScreen(vm: OrdersViewModel = viewModel()) {
val orders = vm.orders // snapshot list — Compose увидит изменения
LazyColumn {
items(items = orders, key = { it.id }) { order -> OrderRow(order) }
}
}
- Observable поля внутри модели (не рекомендуется для слоя данных):
class OrderUiModel(val id: Long, status: Status) {
var status by mutableStateOf(status)
}
// Тогда можно менять orderUiModel.status = newStatus — и item перерисуется,
// но модель становится mutable и UI‑зависимой.
Плюсы и минусы подходов
-
mutableListOf
-
- Простота.
-
− Compose не реагирует; потребуется вручную триггерить обновление.
-
mutableStateListOf / SnapshotStateList
-
- Наблюдаемый список, простые in‑place обновления.
-
- Удобно для UI‑локального состояния.
-
− Привязан к Compose, менее удобен для тестирования и фоновых (потокобезопасных) обновлений.
-
MutableStateFlow<List
> + immutable data class -
- Чистая архитектура, потокобезопасность, легко тестировать.
-
- Хорош для обновлений из фоновых сервисов/Room.
-
− Обновление одного элемента обычно через замену списка (хотя можно оптимизировать).
-
Observable поля в моделях (
mutableStateOf) -
- Удобно для мгновенных, локальных изменений внутри item.
-
− Нарушает разделение слоев, сложнее сериализовать и отлаживать; осторожно с потоками.
Совет: для большинства приложений выбирайте StateFlow + immutable data class + key в LazyColumn. Используйте mutableStateListOf там, где вы уверены, что состояние строго UI‑локально и обновления происходят в UI‑потоке.
Источники
- Parallel processing with state management in Lazy Column - Jetpack Compose — droidcon
- ViewModel and State in Compose | Android Developers (codelab)
- LazyColumn and mutable list – StackOverflow thread (пример обсуждения проблемы)
- Jetpack Compose with ViewModel and Flow — Medium (пример практики)
- Пример блога (доп. материал)
Заключение
Коротко: для изменяемого списка заказов в Jetpack Compose лучшая стартовая точка — хранить состояние в ViewModel и использовать StateFlow<List<Order>> с immutable data class (обновления через copy() + замена в списке). mutableStateListOf подходит для UI‑локальных списков; mutableListOf — не годится, он не вызывает recomposition. Не забывайте про key = { it.id } в LazyColumn, чтобы сохранить локальное состояние элементов при перестановках. Такой подход сочетает предсказуемость, тестируемость и корректную интеграцию с фоновыми обновлениями в приложении на Jetpack Compose.