Урок 5 из 8 · 25 мин · обновлено
Состояние и ViewModel: экран реагирует на действия
Что такое состояние в Compose, как remember и mutableStateOf перерисовывают экран, почему данные пропадают при повороте телефона и как ViewModel хранит состояние экрана.
Состояние
Состояние — это данные, от которых зависит экран: введённый текст, список товаров, отмечен ли пункт. В Compose правило простое: изменилось состояние — Compose сам перерисовал те части экрана, которые от него зависят.
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count++ }) {
Text("Нажато: $count")
}
}
mutableIntStateOf(0)создаёт состояние с начальным значением 0. Для любых других типов естьmutableStateOf(…);rememberзапоминает его между перерисовками. Без него при каждой перерисовке счётчик создавался бы заново с нулём;byпозволяет работать с состоянием как с обычной переменной:count++.
Если сделать count обычной переменной, число на кнопке меняться не будет: Compose не узнает, что что-то изменилось.
Поле ввода
Поле ввода в Compose не хранит текст само. Значение хранит состояние, а поле сообщает о каждом изменении:
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Товар") },
)
value — что показывать, onValueChange — что делать, когда пользователь печатает. Здесь — сохранить новый текст. Такой подход называют однонаправленным потоком данных: данные идут сверху вниз, а события — снизу вверх.
Поворот экрана и ViewModel
Добавьте в список несколько товаров, храня их в remember, и поверните телефон — список исчезнет. При повороте Android пересоздаёт экран, и всё, что хранилось в remember, теряется. Для простых значений есть rememberSaveable, а данные экрана и действия с ними выносят во ViewModel:
class ShoppingViewModel : ViewModel() {
private val _products = MutableStateFlow(listOf(Product("Молоко", 2)))
val products: StateFlow<List<Product>> = _products.asStateFlow()
fun add(name: String) {
_products.update { it + Product(name, 1) }
}
fun delete(product: Product) {
_products.update { it - product }
}
}
- ViewModel переживает поворот экрана и живёт, пока пользователь не ушёл с экрана;
MutableStateFlow— поток данных, который хранит текущее значение и сообщает подписчикам об изменениях. Наружу отдаём версию только для чтения,StateFlow, чтобы менять список можно было лишь через методы ViewModel;update { it + … }создаёт новый список с добавленным товаром. Списки здесь неизменяемые: вместо правки старого создаётся новый.
Экран подписывается на данные и передаёт во ViewModel действия пользователя:
@Composable
fun ShoppingScreen(viewModel: ShoppingViewModel = viewModel()) {
val products by viewModel.products.collectAsStateWithLifecycle()
LazyColumn {
items(products) { product ->
ProductRow(
name = product.name,
count = product.count,
onDelete = { viewModel.delete(product) },
)
}
}
}
viewModel() создаёт ViewModel или возвращает уже существующую, а collectAsStateWithLifecycle() превращает поток в состояние Compose. Теперь кнопка «Удалить» из прошлого урока работает.
Это и есть базовая архитектура Android-приложения: интерфейс только показывает данные и сообщает о нажатиях, а ViewModel хранит данные и решает, что с ними делать.
Попробуйте сами. Добавьте над списком поле ввода и кнопку «Добавить». По нажатию товар попадает в список через ViewModel, а поле очищается. Пустую строку добавлять нельзя.
Решение
@Composable
fun AddProductRow(onAdd: (String) -> Unit) {
var text by remember { mutableStateOf("") }
Row(verticalAlignment = Alignment.CenterVertically) {
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Товар") },
modifier = Modifier.weight(1f),
)
Button(
onClick = {
onAdd(text.trim())
text = ""
},
enabled = text.isNotBlank(),
) {
Text("Добавить")
}
}
}
На экране: AddProductRow(onAdd = { name -> viewModel.add(name) }) над LazyColumn, обоих поместите в Column.
Текст поля — временное состояние, поэтому живёт в remember внутри строки. Список товаров — данные экрана, они во ViewModel. enabled = text.isNotBlank() выключает кнопку, пока поле пустое или в нём одни пробелы.
Мини-тест: проверьте себя
Закрепите урок: несколько вопросов, разбор ответов — в конце.
-
Пользователь нажал кнопку, переменная count увеличилась, но число на экране не поменялось. В чём вероятная причина?
Compose перерисовывает экран, только когда меняется состояние, созданное через mutableStateOf. Обычные переменные он не отслеживает.
-
Что произойдёт с текстом в поле, если в TextField передать value = text, но не обновлять text в onValueChange?
В Compose поле показывает ровно то, что ему передали. Чтобы введённые символы появились, их нужно сохранить в состояние в onValueChange.
-
Почему после поворота телефона список, который хранился в remember, пропал?
Поворот — смена конфигурации: экран создаётся заново. Пережить это помогают rememberSaveable для простых значений и ViewModel для данных экрана.
-
Где по правилам хорошей архитектуры должна жить логика «добавить товар в список»?
ViewModel хранит данные экрана и действия с ними. Интерфейс только показывает данные и сообщает о нажатиях — так код проще проверять и менять.