Обзор ViewModel Часть Android Jetpack .
Класс ViewModel
— это хранилище бизнес-логики или состояния на уровне экрана . Он предоставляет состояние пользовательскому интерфейсу и инкапсулирует связанную бизнес-логику. Его главное преимущество заключается в том, что он кэширует состояние и сохраняет его при изменении конфигурации. Это означает, что пользовательскому интерфейсу не нужно повторно извлекать данные при переходе между действиями или после изменения конфигурации, например, при повороте экрана.
Дополнительную информацию о держателях состояний см. в руководстве по держателям состояний . Аналогично, дополнительную информацию о слое пользовательского интерфейса в целом см. в руководстве по слою пользовательского интерфейса .
Преимущества ViewModel
Альтернативой ViewModel является простой класс, хранящий данные, отображаемые в пользовательском интерфейсе. Это может стать проблемой при навигации между действиями или пунктами назначения навигации. Это приведет к уничтожению данных, если вы не сохраните их с помощью механизма сохранения состояния экземпляра . ViewModel предоставляет удобный API для сохранения данных, который решает эту проблему.
По сути, основных преимуществ класса ViewModel два:
- Позволяет сохранять состояние пользовательского интерфейса.
- Обеспечивает доступ к бизнес-логике.
Упорство
ViewModel обеспечивает персистентность как состояния, хранящегося в ViewModel, так и операций, которые она запускает. Благодаря кэшированию вам не придётся повторно извлекать данные при распространенных изменениях конфигурации, таких как поворот экрана.
Объем
При создании экземпляра ViewModel вы передаёте ему объект, реализующий интерфейс ViewModelStoreOwner
. Это может быть пункт назначения навигации, граф навигации, активность, фрагмент или любой другой тип, реализующий этот интерфейс. Область действия ViewModel ограничивается жизненным циклом ViewModelStoreOwner
. Он остаётся в памяти до тех пор, пока его ViewModelStoreOwner
не будет окончательно удален.
Ряд классов являются прямыми или косвенными подклассами интерфейса ViewModelStoreOwner
. Прямыми подклассами являются ComponentActivity
, Fragment
и NavBackStackEntry
. Полный список косвенных подклассов см. в справочнике ViewModelStoreOwner
.
При уничтожении фрагмента или активности, областью действия которых является ViewModel, асинхронная работа продолжается в той же ViewModel. Это ключ к сохранению.
Более подробную информацию см. в разделе ниже, посвященном жизненному циклу ViewModel .
SavedStateHandle
SavedStateHandle позволяет сохранять данные не только при изменении конфигурации, но и при повторном запуске процесса. То есть, он позволяет сохранять состояние пользовательского интерфейса неизменным, даже когда пользователь закрывает приложение и открывает его позже.
Доступ к бизнес-логике
Несмотря на то, что подавляющее большинство бизнес-логики находится на уровне данных, уровень пользовательского интерфейса также может содержать бизнес-логику. Это может иметь место при объединении данных из нескольких репозиториев для создания состояния пользовательского интерфейса на экране или когда определённый тип данных не требует отдельного уровня данных.
ViewModel — это подходящее место для реализации бизнес-логики на уровне пользовательского интерфейса. ViewModel также отвечает за обработку событий и делегирование их другим уровням иерархии, когда бизнес-логика должна применяться для изменения данных приложения.
Jetpack Compose
При использовании Jetpack Compose ViewModel является основным средством предоставления состояния пользовательского интерфейса на экране вашим компонуемым элементам. В гибридном приложении действия и фрагменты просто размещают ваши компонуемые функции. Это отход от предыдущих подходов, где создание повторно используемых элементов пользовательского интерфейса с помощью действий и фрагментов было не таким простым и интуитивно понятным, что приводило к их значительно более активному использованию в качестве контроллеров пользовательского интерфейса.
Самое важное, что следует помнить при использовании ViewModel с Compose, — это то, что область действия ViewModel не может быть ограничена компонуемым объектом. Это связано с тем, что компонуемый объект не является ViewModelStoreOwner
. Два экземпляра одного и того же компонуемого объекта в Composition или два разных компонуемых объекта, обращающихся к одному и тому же типу ViewModel с одним и тем же ViewModelStoreOwner
, получат один и тот же экземпляр ViewModel, что часто не соответствует ожидаемому поведению.
Чтобы воспользоваться преимуществами ViewModel в Compose, размещайте каждый экран во фрагменте или активности, либо используйте Compose Navigation и используйте ViewModel в компонуемых функциях как можно ближе к целевому объекту навигации. Это связано с тем, что область действия ViewModel может охватывать целевые объекты навигации, графы навигации, активности и фрагменты.
Более подробную информацию см. в руководстве по подъему состояния для Jetpack Compose.
Реализовать ViewModel
Ниже приведен пример реализации ViewModel для экрана, позволяющего пользователю бросать кости.
Котлин
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
// Expose screen UI state
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Handle business logic
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Ява
public class DiceUiState {
private final Integer firstDieValue;
private final Integer secondDieValue;
private final int numberOfRolls;
// ...
}
public class DiceRollViewModel extends ViewModel {
private final MutableLiveData<DiceUiState> uiState =
new MutableLiveData(new DiceUiState(null, null, 0));
public LiveData<DiceUiState> getUiState() {
return uiState;
}
public void rollDice() {
Random random = new Random();
uiState.setValue(
new DiceUiState(
random.nextInt(7) + 1,
random.nextInt(7) + 1,
uiState.getValue().getNumberOfRolls() + 1
)
);
}
}
Затем вы можете получить доступ к ViewModel из действия следующим образом:
Котлин
import androidx.activity.viewModels
class DiceRollActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same DiceRollViewModel instance created by the first activity.
// Use the 'by viewModels()' Kotlin property delegate
// from the activity-ktx artifact
val viewModel: DiceRollViewModel by viewModels()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
Ява
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
model.getUiState().observe(this, uiState -> {
// update UI
});
}
}
Jetpack Compose
import androidx.lifecycle.viewmodel.compose.viewModel
// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
viewModel: DiceRollViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Update UI elements
}
Использование сопрограмм с ViewModel
ViewModel
поддерживает сопрограммы Kotlin. Он способен сохранять асинхронную работу так же, как и состояние пользовательского интерфейса.
Дополнительные сведения см. в разделе Использование сопрограмм Kotlin с компонентами архитектуры Android .
Жизненный цикл ViewModel
Жизненный цикл ViewModel
напрямую связан с областью её действия. ViewModel
остаётся в памяти до тех пор, пока не исчезнет ViewModelStoreOwner
, к которому она относится. Это может произойти в следующих случаях:
- В случае деятельности — когда она заканчивается.
- В случае осколка — когда он отделяется.
- В случае записи навигации, когда она удаляется из стека переходов.
Это делает ViewModels отличным решением для хранения данных, сохраняющих работоспособность при изменении конфигурации.
На рисунке 1 показаны различные состояния жизненного цикла активности, происходящие в процессе её вращения и завершения. На рисунке также показано время жизни ViewModel
рядом с жизненным циклом соответствующей активности. Эта диаграмма иллюстрирует состояния активности. Те же основные состояния применимы и к жизненному циклу фрагмента.
Обычно ViewModel
запрашивается при первом вызове системой метода onCreate()
объекта действия. Система может вызывать onCreate()
несколько раз на протяжении существования действия, например, при повороте экрана устройства. ViewModel
существует с момента первого запроса ViewModel
до завершения и уничтожения действия.
Очистка зависимостей ViewModel
ViewModel вызывает метод onCleared
, когда ViewModelStoreOwner
уничтожает его в ходе своего жизненного цикла. Это позволяет очистить всю работу и зависимости, связанные с жизненным циклом ViewModel.
В следующем примере показана альтернатива viewModelScope
. viewModelScope
— это встроенный CoroutineScope
, который автоматически следует жизненному циклу ViewModel. ViewModel использует его для запуска бизнес-операций. Если для упрощения тестирования вместо viewModelScope
требуется пользовательская область видимости, ViewModel может получить CoroutineScope
в качестве зависимости в своём конструкторе. Когда ViewModelStoreOwner
очищает ViewModel в конце его жизненного цикла, ViewModel также отменяет CoroutineScope
.
class MyViewModel(
private val coroutineScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {
// Other ViewModel logic ...
override fun onCleared() {
coroutineScope.cancel()
}
}
Начиная с версии жизненного цикла 2.5 и выше, вы можете передать один или несколько объектов Closeable
конструктору ViewModel, который автоматически закрывается при очистке экземпляра ViewModel.
class CloseableCoroutineScope(
context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
class MyViewModel(
private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
// Other ViewModel logic ...
}
Лучшие практики
Ниже приведены несколько ключевых рекомендаций, которым следует следовать при реализации ViewModel:
- Ввиду их области действия , ViewModel следует использовать в качестве деталей реализации держателя состояния на уровне экрана. Не используйте их в качестве держателей состояний повторно используемых компонентов пользовательского интерфейса, таких как группы микросхем или формы. В противном случае вы получите один и тот же экземпляр ViewModel в разных случаях использования одного и того же компонента пользовательского интерфейса под одним и тем же ViewModelStoreOwner, если только вы не используете явный ключ модели представления для каждого микросхемы.
- ViewModel не должны знать о деталях реализации пользовательского интерфейса. Старайтесь, чтобы названия методов, предоставляемых API ViewModel, и полей состояния пользовательского интерфейса были максимально общими. Таким образом, ваша ViewModel сможет адаптироваться к любому типу пользовательского интерфейса: мобильному телефону, складному устройству, планшету и даже Chromebook!
- Поскольку они потенциально могут существовать дольше, чем
ViewModelStoreOwner
, ViewModels не должны содержать никаких ссылок на API, связанные с жизненным циклом, такие какContext
илиResources
, чтобы предотвратить утечки памяти. - Не передавайте ViewModel другим классам, функциям или другим компонентам пользовательского интерфейса. Поскольку ими управляет платформа, их следует размещать как можно ближе к ней. Рядом с Activity, фрагментом или компонуемой функцией уровня экрана. Это предотвратит доступ компонентов более низкого уровня к большему объёму данных и логики, чем им необходимо.
Дополнительная информация
По мере усложнения данных вы можете решить использовать отдельный класс только для их загрузки. ViewModel
предназначен для инкапсуляции данных для контроллера пользовательского интерфейса, чтобы они сохранялись при изменении конфигурации. Сведения о загрузке, сохранении и управлении данными при изменении конфигурации см. в разделе «Сохранённые состояния пользовательского интерфейса» .
В руководстве по архитектуре приложений Android предлагается создать класс репозитория для обработки этих функций.
Дополнительные ресурсы
Дополнительную информацию о классе ViewModel
можно найти в следующих ресурсах.
Документация
- слой пользовательского интерфейса
- События пользовательского интерфейса
- Владельцы штатов и штат UI
- Государственное производство
- Уровень данных
Образцы
Рекомендовано для вас
- Примечание: текст ссылки отображается, если JavaScript отключен.
- Используйте сопрограммы Kotlin с компонентами, учитывающими жизненный цикл
- Сохранение состояний пользовательского интерфейса
- Загрузка и отображение постраничных данных