ViewModel позволяет отделить модель данных и логику приложения, связанную с пользовательским интерфейсом, от кода, который отвечает за отображение пользовательского интерфейса и управление им, а также взаимодействие с операционной системой. При таком подходе приложение будет состоять из одного или нескольких объектов Activity, которые представляют пользовательский интерфейс, а также ряда объектов ViewModel, которые будут отвечать за обработку данных. Классы Activity, которые представляют пользовательский интерфейс, отслеживают состояние модели во ViewModel, поэтому любые изменения данных вызывают рекомпозицию. События пользовательского интерфейса, например, нажатие кнопки, настраиваются для вызова соответствующей функции в ViewModel. По сути, это прямая реализация концепции однонаправленного потока данных:
Такое разделение ответственности решает проблемы, связанные с жизненным циклом класса Activity. Независимо от того, сколько раз объект Activity воссоздается в течение жизненного цикла приложения, экземпляры ViewModel остаются в памяти, тем самым обеспечивая согласованность данных. Например, ViewModel, используемая объектом Activity, будет оставаться в памяти до тех пор, пока этот объект Activity не завершит свою работу. А если в приложении только один объект Activity, то ViewModel остается в памяти до конца работы приложения.
ViewModel реализуется как отдельный класс, который содержит данные состояния - данные модели и функции для управления этими данными:
import androidx.lifecycle.ViewModel ................................... class MyViewModel : ViewModel() { }
Основная цель ViewModel — обеспечить работу с данными с помощью пользовательского интерфейса. При этом сами данные могут браться из внешнего источника, например, из базы данных или веб-сервиса. (В этом случае обычно определяется отдельный модуль репозитория, который не связан с ViewModel). Пользовательский интерфейс же может реагировать на изменения данных во ViewModel. Существует два способа объявить данные внутри ViewModel, чтобы их можно было наблюдать:
Определить данные как состояние
Использовать компонент LiveData
При первом подходе данные определяются с помощью функции mutableStateOf() (или аналогичных функциий, которые создают состояние типа State<T>
)
class MyViewModel : ViewModel() { var count by mutableStateOf(0) // состояние ViewModel }
Вместе с состоянием могут обычно определяются функции, которые можно вызывать из пользовательского интерфейса для изменения состояния:
class MyViewModel : ViewModel() { var count by mutableStateOf(0) fun increase() { count++ } }
Определив класс ViewModel, его можно использовать в компонентах пользовательского интерфейса. Однако для его создания нам еще понадобится функция viewModel(), которая по умолчанию
отсутствует в стандартном проекте Compose. Поэтому нам вручную надо добавить соответствующую зависимость в файл build.gradle. Поэтому вначале откроем в проекте файл
libs.version.toml и в секцию [libraries]
добавим определение библиотеки:
[libraries] androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } .......................
Далее откроем файл build.gradle.kts (Module :app) и в секцию dependencies добавим соответствующую зависимость:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
То есть в итоге получится
dependencies { implementation(libs.androidx.lifecycle.viewmodel.compose) // остальные зависимости ........................................ }
Затем синхронизируем проект, нажав на кнопку Sync Now
После добавления зависимости мы можем использовать ViewModel. Для этого надо передать экземпляр ViewModel в качестве параметра компоненту, из которого можно получить доступ к значениям состояния и функциям. Причем рекомендуется передавать ViewModel в компонент, который находится на вершине иерархии компонентов. А из этого компонента при необходимости состояние и функции для управления состоянием могут быть переданы дочерним компонентам. Например:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.runtime.Composable class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CounterView() } } } class CounterViewModel : ViewModel() { var count by mutableStateOf(0) fun increase() { count++ } } @Composable fun CounterView(vm: CounterViewModel = viewModel()) { Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Text("Count: ${vm.count}", Modifier.padding(10.dp), fontSize = 25.sp) Button(onClick = {vm.increase()}) { Text(text = "Increase", fontSize = 25.sp) } } }
Здесь определен компонент верхнего уровня CounterView, который инкапсулирует весь остальной интерфейс. В этот компонент в качестве параметра передается объект CounterViewModel - наш ViewModel, который определяет состояние - переменную count и функцию increase для изменения этого состояния. Причем для создания CounterViewModel применяется не его конструктор, а функция viewModel(). Если экземпляр ViewModel (в нашем случае CounterViewModel) уже создан в текущем контексте, то функция viewModel() вернет ссылку на этот экземпляр. В противном случае будет создан и возвращен новый экземпляр ViewModel.
Внутри CounterView определяем текстовую метку, которая выводит состояние, и кнопку, которая вызывает функцию increase. Таким образом, при нажатии на кнопку произойдет вызов функции increase, которая, в свою очередь, увеличивает состояние count. Это изменение состояния вызывает рекомпозицию пользовательского интерфейса, в результате чего новое значение count отобразится в компоненте Text: