Мобильная разработка

Jetpack Compose: изменяемый список в LazyColumn

Как правильно отображать изменяемый список заказов в Jetpack Compose с LazyColumn. Обновления элементов, ViewModel, StateFlow, mutableStateListOf, key. Примеры кода, плюсы и минусы подходов для фоновых обновлений.

Как правильно в Jetpack Compose отображать изменяемый список заказов и отслеживать обновления отдельных элементов?

Контекст:

  • Загружаю по API список заказов как обычные POJO и отображаю их в LazyColumn.
  • Список может меняться (добавление/удаление/изменение порядка), и поля отдельных заказов тоже могут меняться (например, фоновая служба обновляет status).

Вопросы:

  1. Какой подход считается правильным, чтобы Compose автоматически перерисовывал список и элементы при изменениях? Достаточно ли использовать mutableListOf, или лучше mutableStateListOf / SnapshotStateList / Snapshot state?
  2. При изменении полей заказа: стоит ли делать поля модели observable (например, var status by mutableStateOf(…)) внутри POJO, или лучше использовать неизменяемые data class и при обновлении заменять объект в списке (copy + замена в состоянии)?
  3. Нужен ли key в LazyColumn в таких сценариях и как правильно его использовать?
  4. Какие архитектурные практики/паттерны вы порекомендуете (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 отслеживает изменения (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):

kotlin
// 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:

kotlin
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>> (рекомендуется для архитектуры):

kotlin
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)

Есть два распространённых подхода для модели элемента:

  1. 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), легкость тестирования, предсказуемость.
  • Минусы: при очень частых изменениях возможны аллокации новых списков/объектов (обычно не проблема).
  1. 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 связывал состояние элементов по стабильному идентификатору, а не по индексу.

Пример:

kotlin
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‑состояние на главном потоке, если нужно.

Пример потока обновлений из репозитория:

kotlin
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.

Короткие примеры кода

  1. StateFlow + immutable data class (рекомендуемый):
kotlin
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)
 }
 }
}
  1. Snapshot state list (mutableStateListOf):
kotlin
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) }
 }
}
  1. Observable поля внутри модели (не рекомендуется для слоя данных):
kotlin
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‑потоке.


Источники


Заключение

Коротко: для изменяемого списка заказов в Jetpack Compose лучшая стартовая точка — хранить состояние в ViewModel и использовать StateFlow<List<Order>> с immutable data class (обновления через copy() + замена в списке). mutableStateListOf подходит для UI‑локальных списков; mutableListOf — не годится, он не вызывает recomposition. Не забывайте про key = { it.id } в LazyColumn, чтобы сохранить локальное состояние элементов при перестановках. Такой подход сочетает предсказуемость, тестируемость и корректную интеграцию с фоновыми обновлениями в приложении на Jetpack Compose.

Авторы
Проверено модерацией
Модерация
Jetpack Compose: изменяемый список в LazyColumn