Когда вы впервые сталкиваетесь с разработкой под Android, то обычно весь код пишете прямо в Activity или в Composable-функциях. Это работает, пока приложение маленькое, но как только экранов становится больше, такой подход приводит к путанице. Данные теряются при повороте экрана, логика перемешивается с отображением, а тестировать такой код почти невозможно. В этом уроке из цикла Android Studio Kotlin уроки мы познакомимся с ViewModel — инструментом, который решает эти проблемы, и научимся использовать StateFlow для реактивного обновления интерфейса.
Зачем нужен ViewModel
ViewModel — это специальный класс из библиотеки Jetpack, предназначенный для хранения и управления данными, связанными с пользовательским интерфейсом. Его главное преимущество: ViewModel живёт дольше, чем Activity или Fragment. Когда вы поворачиваете экран, система пересоздаёт Activity, но ваш ViewModel остаётся неизменным, и все данные, которые он хранит, не пропадают. Это избавляет от необходимости вручную сохранять состояние в Bundle и восстанавливать его.
Кроме того, ViewModel хорошо разделяет обязанности: Activity или Composable занимаются только отображением, а ViewModel — всей логикой и данными. Это делает код чище и удобнее для тестирования.
Добавляем зависимости
Для работы с ViewModel в проекте на Kotlin необходимо подключить библиотеку lifecycle-viewmodel-compose. Она содержит всё необходимое для использования ViewModel в Jetpack Compose. Откройте файл build.gradle.kts уровня модуля и добавьте строку:
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
}
После синхронизации Gradle библиотека будет готова к работе. Никаких дополнительных настроек не требуется.
Создаём первое ViewModel
Создадим простой экран-счётчик. Пользователь нажимает на кнопку, и число увеличивается. Без ViewModel это выглядело бы как переменная внутри Composable-функции. Но при повороте экрана счётчик сбрасывался бы. Мы же сделаем по-другому.
Сначала создадим класс, наследник от ViewModel. Внутри будем хранить состояние счётчика. Можно было бы использовать простой Int, но для реактивности применяем MutableStateFlow. StateFlow — это поток данных, на который можно подписаться из интерфейса. Когда значение меняется, подписчики автоматически обновляются.
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count = _count.asStateFlow()
fun increment() {
_count.value++
}
}
Обратите внимание: _count — приватная изменяемая переменная, а count — публичная неизменяемая версия. Так другие части программы могут только читать значение, но не менять его в обход ViewModel.
Подключаем ViewModel к экрану Compose
Теперь напишем интерфейс. В Jetpack Compose получить экземпляр ViewModel можно с помощью функции viewModel(). Она автоматически свяжет ViewModel с текущим жизненным циклом Activity или Fragment.
import androidx.compose.runtime.*
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CounterScreen() {
val viewModel: CounterViewModel = viewModel()
val count by viewModel.count.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Счётчик: $count", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.increment() }) {
Text("Увеличить")
}
}
}
Разберём ключевой момент: collectAsState() — это специальная Compose-функция, которая подписывается на StateFlow и превращает его в состояние Compose. При изменении данных в StateFlow Compose автоматически перерисует нужные части экрана.
Теперь запустите приложение, нажмите несколько раз кнопку, а затем поверните устройство. Счётчик останется неизменным — ViewModel сохранил его.
Передаём параметры в ViewModel
Часто ViewModel нужно стартовое значение, например идентификатор товара, переданный с предыдущего экрана. Для этого существует ViewModelProvider.Factory. Вручную писать фабрику на каждый класс утомительно, поэтому используют viewModel() с дополнительными параметрами через библиотеку lifecycle-viewmodel-compose — она умеет принимать параметры в ключ-значении.
Но для нашего урока сделаем проще: пока просто будем задавать начальное значение нулём, а в реальном проекте можно воспользоваться SavedStateHandle (он доступен внутри ViewModel и автоматически сохраняет данные в Bundle).
Реакция на события и загрузка данных
Расширим пример. Пусть при нажатии кнопки «Загрузить» счётчик устанавливается в случайное значение от 1 до 100. Для имитации задержки используем корутину внутри ViewModel. Для этого ViewModel предоставляет viewModelScope — специальный скоуп корутин, который автоматически отменяется, когда ViewModel уничтожается.
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count = _count.asStateFlow()
fun increment() {
_count.value++
}
fun loadRandom() {
viewModelScope.launch {
delay(500) // имитация сетевого запроса
_count.value = (1..100).random()
}
}
}
В интерфейсе добавим ещё одну кнопку и вызовем viewModel.loadRandom(). Теперь у нас два действия: одно синхронное, другое асинхронное с задержкой. Обратите внимание, что интерфейс остаётся отзывчивым на всё время выполнения загрузки.
Обработка ошибок и состояние загрузки
Хорошим тоном считается отображать процесс загрузки и ошибки. Для этого в StateFlow можно хранить не просто число, а специальный sealed-класс состояния. Создадим такой:
sealed class UiState {
data object Loading : UiState()
data class Success(val value: Int) : UiState()
data class Error(val message: String) : UiState()
}
Переделаем ViewModel под использование этого состояния:
class CounterViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Success(0))
val uiState = _uiState.asStateFlow()
fun increment() {
val current = (_uiState.value as? UiState.Success)?.value ?: 0
_uiState.value = UiState.Success(current + 1)
}
fun loadRandom() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
delay(500)
_uiState.value = UiState.Success((1..100).random())
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Ошибка")
}
}
}
}
А в Compose будем обрабатывать состояние через when:
val uiState by viewModel.uiState.collectAsState()
when (val state = uiState) {
is UiState.Loading -> Text("Загрузка...")
is UiState.Success -> Text("Счётчик: ${state.value}")
is UiState.Error -> Text("Ошибка: ${state.message}")
}
Теперь интерфейс чётко отражает текущее состояние: загрузка, успех или ошибка. Это делает приложение понятным и устойчивым.
Краткий конспект: что мы изучили
- ViewModel хранит данные и переживает поворот экрана.
- StateFlow позволяет реактивно обновлять интерфейс при изменении данных.
collectAsState()превращает Flow в состояние Compose.- viewModelScope предназначен для безопасного выполнения корутин в ViewModel.
- Паттерн с sealed-классом для состояния помогает обрабатывать загрузку и ошибки.
Этот урок из цикла Android Studio Kotlin уроки дал вам фундамент для построения надёжных и поддерживаемых приложений. Теперь вы можете вынести логику из Activity в ViewModel, а обновление интерфейса сделать реактивным. Попробуйте самостоятельно добавить сброс счётчика и кнопку «Назад» — это закрепит навык.