Современные пользовательские интерфейсы редко бывают статичными. Состояние пользовательского интерфейса меняется, когда пользователь взаимодействует с пользовательским интерфейсом или когда приложению необходимо отобразить новые данные.
Этот документ описывает рекомендации по созданию и управлению состоянием пользовательского интерфейса. В конце вам следует:
- Знайте, какие API вам следует использовать для создания состояния пользовательского интерфейса. Это зависит от характера источников изменения состояния, доступных в ваших держателях состояний, в соответствии с принципами однонаправленного потока данных .
- Знайте, как следует определять границы создания состояния пользовательского интерфейса, чтобы учитывать системные ресурсы.
- Знайте, как вы должны предоставлять состояние пользовательского интерфейса для использования в пользовательском интерфейсе.
По сути, производство состояний — это постепенное применение этих изменений к состоянию пользовательского интерфейса. Государство существует всегда и меняется в результате событий. Различия между событиями и состояниями приведены в таблице ниже:
События | Состояние |
---|---|
Преходящи, непредсказуемы и существуют в течение конечного периода. | Всегда существует. |
Затраты государственного производства. | Продукция государственного производства. |
Продукт пользовательского интерфейса или других источников. | Используется пользовательским интерфейсом. |
Отличная мнемоника, которая суммирует вышесказанное, — это состояние; события происходят . Диаграмма ниже помогает визуализировать изменения состояния по мере того, как события происходят на временной шкале. Каждое событие обрабатывается соответствующим держателем состояния и приводит к изменению состояния:
События могут исходить из:
- Пользователи : когда они взаимодействуют с пользовательским интерфейсом приложения.
- Другие источники изменения состояния : API, которые представляют данные приложения из пользовательского интерфейса, домена или слоев данных, таких как события тайм-аута закусочной, варианты использования или репозитории соответственно.
Конвейер производства состояний пользовательского интерфейса
Производство состояний в приложениях Android можно рассматривать как конвейер обработки, включающий:
- Входные данные : источники изменения состояния. Они могут быть:
- Локально для уровня пользовательского интерфейса: это могут быть пользовательские события, такие как ввод пользователем названия задачи в приложении для управления задачами, или API-интерфейсы, которые предоставляют доступ к логике пользовательского интерфейса , которая приводит к изменениям в состоянии пользовательского интерфейса. Например, вызов метода
open
дляDrawerState
в Jetpack Compose. - Внешние по отношению к уровню пользовательского интерфейса: это источники из уровня домена или данных, которые вызывают изменения состояния пользовательского интерфейса. Например, новости, которые завершили загрузку из
NewsRepository
, или другие события. - Смесь всего вышеперечисленного.
- Локально для уровня пользовательского интерфейса: это могут быть пользовательские события, такие как ввод пользователем названия задачи в приложении для управления задачами, или API-интерфейсы, которые предоставляют доступ к логике пользовательского интерфейса , которая приводит к изменениям в состоянии пользовательского интерфейса. Например, вызов метода
- Держатели состояний : типы, которые применяют бизнес-логику и/или логику пользовательского интерфейса к источникам изменения состояния и обрабатывают пользовательские события для создания состояния пользовательского интерфейса.
- Выходные данные : состояние пользовательского интерфейса, которое приложение может отображать, чтобы предоставить пользователям необходимую информацию.
API государственного производства
В производстве состояний используются два основных API, в зависимости от того, на каком этапе конвейера вы находитесь:
Этап конвейера | API |
---|---|
Вход | Вам следует использовать асинхронные API для выполнения работы вне потока пользовательского интерфейса, чтобы избежать зависаний пользовательского интерфейса. Например, сопрограммы или потоки в Kotlin и RxJava или обратные вызовы в языке программирования Java. |
Выход | Вам следует использовать API-интерфейсы наблюдаемых держателей данных, чтобы сделать недействительным и повторно отобразить пользовательский интерфейс при изменении состояния. Например, StateFlow, Compose State или LiveData. Наблюдаемые держатели данных гарантируют, что пользовательский интерфейс всегда имеет состояние пользовательского интерфейса для отображения на экране. |
Из этих двух вариантов выбор асинхронного API для ввода оказывает большее влияние на характер производственного конвейера состояния, чем выбор наблюдаемого API для вывода. Это связано с тем, что входные данные определяют тип обработки, которая может быть применена к конвейеру .
Монтаж государственного производственного трубопровода
В следующих разделах рассматриваются методы создания состояний, которые лучше всего подходят для различных входных данных, а также соответствующие выходные API. Каждый государственный производственный конвейер представляет собой комбинацию входов и выходов и должен:
- Осведомленность о жизненном цикле : в случае, когда пользовательский интерфейс не виден и не активен, производственный конвейер состояния не должен потреблять какие-либо ресурсы, если это явно не требуется.
- Простота использования : пользовательский интерфейс должен иметь возможность легко отображать созданное состояние пользовательского интерфейса. Рекомендации по выходным данным производственного конвейера состояния будут различаться в зависимости от различных API-интерфейсов View, таких как система View или Jetpack Compose.
Затраты в государственные производственные трубопроводы
Входные данные в конвейере производства состояний могут либо обеспечивать источники изменения состояния посредством:
- Одноразовые операции, которые могут быть синхронными или асинхронными, например вызовы
suspend
функций. - Stream API, например
Flows
. - Все вышеперечисленное.
В следующих разделах описано, как собрать конвейер производства состояний для каждого из вышеперечисленных входных данных.
Одноразовые API как источники изменения состояния
Используйте API MutableStateFlow
в качестве наблюдаемого изменяемого контейнера состояния. В приложениях Jetpack Compose вы также можете рассмотреть mutableStateOf
особенно при работе с API Compose text . Оба API предлагают методы, которые позволяют безопасно атомарно обновлять значения, которые они размещают, независимо от того, являются ли обновления синхронными или асинхронными.
Например, рассмотрим обновление состояния в простом приложении для игры в кости. Каждый бросок кубика пользователем вызывает синхронный метод Random.nextInt()
, и результат записывается в состояние пользовательского интерфейса.
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
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,
)
}
}
}
Составление состояния
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
Изменение состояния пользовательского интерфейса из асинхронных вызовов
Для изменений состояния, требующих асинхронного результата, запустите Coroutine в соответствующем CoroutineScope
. Это позволяет приложению отменить работу при отмене CoroutineScope
. Затем держатель состояния записывает результат вызова метода приостановки в наблюдаемый API, используемый для раскрытия состояния пользовательского интерфейса.
Например, рассмотрим AddEditTaskViewModel
в примере архитектуры . Когда приостанавливающий метод saveTask()
сохраняет задачу асинхронно, метод update
MutableStateFlow передает изменение состояния в состояние пользовательского интерфейса.
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
Составление состояния
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
Изменение состояния пользовательского интерфейса из фоновых потоков
Предпочтительно запускать Coroutines в главном диспетчере для создания состояния пользовательского интерфейса. То есть за пределами блока withContext
в приведенных ниже фрагментах кода. Однако если вам нужно обновить состояние пользовательского интерфейса в другом фоновом контексте, вы можете сделать это, используя следующие API:
- Используйте метод
withContext
для запуска сопрограмм в другом параллельном контексте. - При использовании
MutableStateFlow
используйте методupdate
как обычно. - При использовании Compose State используйте
Snapshot.withMutableSnapshot
, чтобы гарантировать атомарные обновления состояния в параллельном контексте.
Например, предположим, что в приведенном ниже фрагменте DiceRollViewModel
SlowRandom.nextInt()
— это функция suspend
с интенсивными вычислениями, которую необходимо вызывать из корутины, связанной с ЦП.
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
Составление состояния
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
Потоковые API как источники изменения состояния
Для источников изменения состояния, которые с течением времени производят несколько значений в потоках, агрегирование результатов всех источников в единое целое является простым подходом к созданию состояний.
При использовании Kotlin Flows этого можно добиться с помощью функции объединения . Пример этого можно увидеть в примере «Сейчас в Android» в InterestsViewModel:
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
Использование оператора stateIn
для создания StateFlows
дает пользовательскому интерфейсу более детальный контроль над деятельностью конвейера производства состояний, поскольку ему может потребоваться быть активным только тогда, когда пользовательский интерфейс виден.
- Используйте
SharingStarted.WhileSubscribed()
, если конвейер должен быть активен только тогда, когда пользовательский интерфейс виден, при сборе потока с учетом жизненного цикла. - Используйте
SharingStarted.Lazily
, если конвейер должен быть активен, пока пользователь может вернуться к пользовательскому интерфейсу, то есть пользовательский интерфейс находится в стеке или на другой вкладке за пределами экрана.
В тех случаях, когда агрегирование источников состояния на основе потоков не применяется, потоковые API, такие как Kotlin Flows, предлагают богатый набор преобразований, таких как слияние , сглаживание и т. д., чтобы помочь в обработке потоков в состояние пользовательского интерфейса.
Одноразовые и потоковые API как источники изменения состояния
В случае, когда конвейер производства состояний зависит как от одноразовых вызовов, так и от потоков как источников изменения состояния, потоки являются определяющим ограничением. Поэтому преобразуйте одноразовые вызовы в API потоков или перенаправьте их выходные данные в потоки и возобновите обработку, как описано в разделе потоков выше.
В случае с потоками это обычно означает создание одного или нескольких частных резервных экземпляров MutableStateFlow
для распространения изменений состояния. Вы также можете создавать потоки снимков из состояния Compose.
Рассмотрим TaskDetailViewModel
из репозитория образцов архитектуры ниже:
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
Составление состояния
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
Типы продукции в государственных производственных конвейерах
Выбор выходного API для состояния пользовательского интерфейса и характер его представления во многом зависят от API, который ваше приложение использует для визуализации пользовательского интерфейса. В приложениях Android вы можете использовать Views или Jetpack Compose. Здесь следует учитывать следующее:
- Чтение состояния с учетом жизненного цикла .
- Должно ли состояние быть раскрыто в одном или нескольких полях от владельца состояния.
В следующей таблице приведены API-интерфейсы, которые следует использовать для конвейера производства состояний для любого заданного ввода и потребителя:
Вход | Потребитель | Выход |
---|---|---|
Одноразовые API | Просмотры | StateFlow или LiveData |
Одноразовые API | Сочинить | StateFlow или Compose State |
Потоковые API | Просмотры | StateFlow или LiveData |
Потоковые API | Сочинить | StateFlow |
Одноразовые и потоковые API | Просмотры | StateFlow или LiveData |
Одноразовые и потоковые API | Сочинить | StateFlow |
Инициализация государственного производственного конвейера
Инициализация государственных производственных конвейеров включает в себя установку начальных условий для работы конвейера. Это может включать предоставление начальных входных значений, важных для запуска конвейера, например id
для подробного представления новостной статьи, или запуск асинхронной загрузки.
По возможности вам следует инициализировать производственный конвейер состояния лениво, чтобы сохранить системные ресурсы. На практике это часто означает ожидание, пока не появится потребитель продукции. API-интерфейсы Flow
позволяют это сделать с помощью аргумента started
в методе stateIn
. В тех случаях, когда это неприменимо, определите идемпотентную функцию initialize()
, чтобы явно запустить конвейер создания состояний, как показано в следующем фрагменте:
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
Образцы
Следующие примеры Google демонстрируют создание состояния на уровне пользовательского интерфейса. Изучите их, чтобы увидеть это руководство на практике:
Рекомендуется для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Уровень пользовательского интерфейса
- Создайте оффлайн-приложение
- Держатели состояний и состояние пользовательского интерфейса {:#mad-arch}