В руководстве по слою пользовательского интерфейса рассматривается однонаправленный поток данных (UDF) как средство создания и управления состоянием пользовательского интерфейса.

В документе также подчеркиваются преимущества делегирования управления пользовательскими функциями (UDF) специальному классу, называемому держателем состояния. Держатель состояния можно реализовать либо через ViewModel , либо через обычный класс. В этом документе более подробно рассматриваются держатели состояния и их роль в пользовательском интерфейсе.
По завершении работы с этим документом вы должны понимать, как управлять состоянием приложения на уровне пользовательского интерфейса; то есть, конвейером создания состояния пользовательского интерфейса. Вы должны уметь понимать и знать следующее:
- Разберитесь в типах состояний пользовательского интерфейса, существующих на уровне пользовательского интерфейса.
- Разберитесь в типах логики, которая работает с этими состояниями пользовательского интерфейса на уровне пользовательского интерфейса.
- Умейте выбирать подходящую реализацию хранилища состояния, например,
ViewModelили класс.
Элементы конвейера производства состояния пользовательского интерфейса
Состояние пользовательского интерфейса и логика, которая его формирует, определяют слой пользовательского интерфейса.
состояние пользовательского интерфейса
Состояние пользовательского интерфейса — это свойство, описывающее пользовательский интерфейс. Существует два типа состояния пользовательского интерфейса:
- Состояние пользовательского интерфейса экрана — это то, что необходимо отобразить на экране. Например, класс
NewsUiStateможет содержать новостные статьи и другую информацию, необходимую для отображения пользовательского интерфейса. Это состояние обычно связано с другими уровнями иерархии, поскольку содержит данные приложения. - Состояние элемента пользовательского интерфейса относится к свойствам, присущим элементам пользовательского интерфейса и влияющим на их отображение. Элемент пользовательского интерфейса может быть показан или скрыт, а также может иметь определенный шрифт, размер шрифта или цвет шрифта. В Jetpack Compose состояние находится вне компонуемого объекта, и вы даже можете переместить его из непосредственной близости от компонуемого объекта в вызывающую функцию компонуемого объекта или в контейнер состояния. Примером этого является
ScaffoldStateдля компонуемого объектаScaffold.
Логика
Состояние пользовательского интерфейса не является статическим свойством, поскольку данные приложения и события пользователя приводят к изменению состояния интерфейса с течением времени. Логика определяет конкретные детали изменения, включая то, какие части состояния пользовательского интерфейса изменились, почему это произошло и когда это изменение должно произойти.

Логика в приложении может быть либо бизнес-логикой, либо логикой пользовательского интерфейса:
- Бизнес-логика — это реализация требований к продукту для данных приложения. Например, добавление статьи в закладки в новостном приложении при нажатии пользователем кнопки. Эта логика сохранения закладки в файл или базу данных обычно размещается на уровне предметной области или данных. Ответственный за состояние обычно делегирует эту логику этим уровням, вызывая предоставляемые ими методы.
- Логика пользовательского интерфейса связана с тем, как отображать состояние интерфейса на экране. Например, получение подсказки в строке поиска, когда пользователь выбрал категорию, прокрутка до определенного элемента в списке или логика навигации к определенному экрану при нажатии пользователем кнопки.
Жизненный цикл Android и типы состояния и логики пользовательского интерфейса.
Слой пользовательского интерфейса состоит из двух частей: одна зависит от жизненного цикла интерфейса, а другая не зависит от него. Это разделение определяет источники данных, доступные каждой части, и, следовательно, требует различных типов состояния и логики пользовательского интерфейса.
- Независимый от жизненного цикла пользовательский интерфейс : эта часть слоя пользовательского интерфейса отвечает за слои, генерирующие данные приложения (слои данных или предметной области), и определяется бизнес-логикой. Жизненный цикл, изменения конфигурации и создание
Activityв пользовательском интерфейсе могут влиять на активность конвейера генерации состояния пользовательского интерфейса, но не влияют на достоверность генерируемых данных. - Зависимость от жизненного цикла пользовательского интерфейса : Эта часть слоя пользовательского интерфейса отвечает за логику интерфейса и напрямую зависит от изменений жизненного цикла или конфигурации. Эти изменения напрямую влияют на корректность считываемых данных, и, как следствие, состояние интерфейса может изменяться только тогда, когда активен его жизненный цикл. Примерами этого являются разрешения во время выполнения и получение ресурсов, зависящих от конфигурации, таких как локализованные строки.
Вышеизложенное можно обобщить с помощью приведенной ниже таблицы:
| Независимый от жизненного цикла пользовательского интерфейса | Зависит от жизненного цикла пользовательского интерфейса. |
|---|---|
| Бизнес-логика | Логика пользовательского интерфейса |
| Состояние пользовательского интерфейса экрана |
Конвейер производства состояния пользовательского интерфейса
Конвейер создания состояния пользовательского интерфейса (UI) описывает шаги, предпринимаемые для формирования состояния UI. Эти шаги включают применение описанной ранее логики и полностью зависят от потребностей вашего UI. Некоторые UI могут использовать как независимые от жизненного цикла UI, так и зависимые от него части конвейера, либо ни одну из них, либо ни одну из них .
То есть, допустимы следующие варианты построения конвейера слоя пользовательского интерфейса:
Состояние пользовательского интерфейса создается и управляется самим интерфейсом. Например, простой, многократно используемый базовый счетчик:
@Composable fun Counter() { // The UI state is managed by the UI itself var count by remember { mutableStateOf(0) } Row { Button(onClick = { ++count }) { Text(text = "Increment") } Button(onClick = { --count }) { Text(text = "Decrement") } } }Логика пользовательского интерфейса → UI. Например, отображение или скрытие кнопки, позволяющей пользователю перейти в начало списка.
@Composable fun ContactsList(contacts: List<Contact>) { val listState = rememberLazyListState() val isAtTopOfList by remember { derivedStateOf { listState.firstVisibleItemIndex < 3 } } // Create the LazyColumn with the lazyListState ... // Show or hide the button (UI logic) based on the list scroll position AnimatedVisibility(visible = !isAtTopOfList) { ScrollToTopButton() } }Бизнес-логика → Пользовательский интерфейс. Элемент пользовательского интерфейса, отображающий фотографию текущего пользователя на экране.
@Composable fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) { // Read screen UI state from the business logic state holder val uiState by viewModel.uiState.collectAsStateWithLifecycle() // Call on the UserAvatar Composable to display the photo UserAvatar(picture = uiState.profilePicture) }Бизнес-логика → Логика пользовательского интерфейса → Пользовательский интерфейс. Элемент пользовательского интерфейса, который прокручивается для отображения нужной информации на экране в зависимости от заданного состояния интерфейса.
@Composable fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) { // Read screen UI state from the business logic state holder val uiState by viewModel.uiState.collectAsStateWithLifecycle() val contacts = uiState.contacts val deepLinkedContact = uiState.deepLinkedContact val listState = rememberLazyListState() // Create the LazyColumn with the lazyListState ... // Perform UI logic that depends on information from business logic if (deepLinkedContact != null && contacts.isNotEmpty()) { LaunchedEffect(listState, deepLinkedContact, contacts) { val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact) if (deepLinkedContactIndex >= 0) { // Scroll to deep linked item listState.animateScrollToItem(deepLinkedContactIndex) } } } }
В случае, когда к конвейеру формирования состояния пользовательского интерфейса применяются оба типа логики, бизнес-логика всегда должна применяться до логики пользовательского интерфейса . Попытка применить бизнес-логику после логики пользовательского интерфейса будет означать, что бизнес-логика зависит от логики пользовательского интерфейса. В следующих разделах подробно рассматривается, почему это является проблемой, на примере различных типов логики и их держателей состояний.

Государственные обладатели и их обязанности
Задача владельца состояния — хранить состояние, чтобы приложение могло его считывать. В случаях, когда необходима логика, он выступает в качестве посредника и предоставляет доступ к источникам данных, содержащим необходимую логику. Таким образом, владелец состояния делегирует логику соответствующему источнику данных.
Это дает следующие преимущества:
- Простые пользовательские интерфейсы : интерфейс просто привязывает свое состояние.
- Поддерживаемость : Логику, определенную в содержании состояния, можно изменять итеративно, не внося изменений в сам пользовательский интерфейс.
- Тестируемость : пользовательский интерфейс и логика генерации состояний могут быть протестированы независимо друг от друга.
- Читаемость : Читатели кода могут четко увидеть различия между кодом отображения пользовательского интерфейса и кодом создания состояния пользовательского интерфейса.
Независимо от размера или масштаба, каждый элемент пользовательского интерфейса имеет отношение «один к одному» со своим соответствующим владельцем состояния. Более того, владелец состояния должен быть способен принимать и обрабатывать любые действия пользователя, которые могут привести к изменению состояния пользовательского интерфейса, и должен обеспечивать последующее изменение состояния.
Типы держателей государственных активов
Подобно типам состояний и логики пользовательского интерфейса, в слое пользовательского интерфейса существуют два типа хранителей состояний, определяемые их связью с жизненным циклом пользовательского интерфейса:
- Хранитель состояния бизнес-логики.
- Хранитель состояния логики пользовательского интерфейса.
В следующих разделах более подробно рассматриваются типы держателей состояний, начиная с держателя состояния бизнес-логики.
Бизнес-логика и её владелец состояния
Содержимое, использующее бизнес-логику, обрабатывает пользовательские события и преобразует данные из слоев данных или предметной области в состояние пользовательского интерфейса экрана. Для обеспечения оптимального пользовательского опыта с учетом жизненного цикла Android и изменений конфигурации приложения, содержимое, использующее бизнес-логику, должно обладать следующими свойствами:
| Свойство | Деталь |
|---|---|
| Создает состояние пользовательского интерфейса | Ответственность за формирование состояния пользовательского интерфейса лежит на разработчиках бизнес-логики. Это состояние часто является результатом обработки пользовательских событий и чтения данных из предметной области и уровня данных. |
| Сохраняется благодаря активному отдыху. | Хранители состояний бизнес-логики сохраняют свое состояние и конвейеры обработки состояний при повторном создании Activity , что помогает обеспечить бесперебойную работу пользователя. В случаях, когда хранение состояния становится невозможным и происходит его повторное создание (обычно после завершения процесса ), хранитель состояния должен иметь возможность легко воссоздать свое последнее состояние, чтобы обеспечить согласованный пользовательский опыт. |
| Обладать долгоживущим состоянием | Хранители состояний бизнес-логики часто используются для управления состоянием навигационных элементов. В результате они часто сохраняют свое состояние при изменении навигации до тех пор, пока не будут удалены из графа навигации. |
| Этот интерфейс уникален и не подлежит повторному использованию. | Хранители состояний бизнес-логики обычно создают состояние для определенной функции приложения, например, TaskEditViewModel или TaskListViewModel , и, следовательно, применимы только к этой функции приложения. Один и тот же хранитель состояний может поддерживать эти функции приложения на разных форм-факторах. Например, мобильные, телевизионные и планшетные версии приложения могут повторно использовать один и тот же хранитель состояний бизнес-логики. |
Например, рассмотрим навигационную панель автора в приложении "Теперь в Android ":

В данном случае AuthorViewModel , выступая в роли хранителя состояния бизнес-логики, формирует состояние пользовательского интерфейса:
@HiltViewModel
class AuthorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authorsRepository: AuthorsRepository,
newsRepository: NewsRepository
) : ViewModel() {
val uiState: StateFlow<AuthorScreenUiState> = …
// Business logic
fun followAuthor(followed: Boolean) {
…
}
}
Обратите внимание, что AuthorViewModel обладает атрибутами, описанными ранее:
| Свойство | Деталь |
|---|---|
Создает AuthorScreenUiState | AuthorViewModel считывает данные из AuthorsRepository и NewsRepository и использует эти данные для создания AuthorScreenUiState . Он также применяет бизнес-логику, когда пользователь хочет подписаться или отписаться Author делегируя задачу AuthorsRepository . |
| Имеет доступ к слою данных. | В конструктор объекта передаются экземпляры AuthorsRepository и NewsRepository , что позволяет реализовать бизнес-логику отслеживания Author . |
Выживает Activity отдых | Поскольку он реализован с использованием ViewModel , он будет сохраняться при быстром создании Activity . В случае завершения процесса можно считать данные из объекта SavedStateHandle , чтобы получить минимальное количество информации, необходимой для восстановления состояния пользовательского интерфейса из слоя данных. |
| Обладает долгоживущим состоянием | ViewModel ограничен областью видимости графа навигации, поэтому, если целевая страница автора не будет удалена из графа навигации, состояние пользовательского интерфейса в StateFlow uiState остается в памяти. Использование StateFlow также дает преимущество в виде отложенного выполнения бизнес-логики, генерирующей состояние, поскольку состояние генерируется только при наличии сборщика состояния пользовательского интерфейса. |
| Уникален своим пользовательским интерфейсом. | AuthorViewModel применим только к навигационному меню автора и не может быть использован повторно где-либо еще. Если какая-либо бизнес-логика используется повторно в разных навигационных меню, эта бизнес-логика должна быть инкапсулирована в компонент, находящийся в области данных или предметной области. |
ViewModel как хранилище состояния бизнес-логики
Преимущества ViewModel в разработке под Android делают их подходящими для предоставления доступа к бизнес-логике и подготовки данных приложения к отображению на экране. К этим преимуществам относятся следующие:
- Операции, запускаемые ViewModel, сохраняются при изменении конфигурации.
- Интеграция с навигацией :
- Навигация кэширует ViewModels, пока экран находится в стеке возврата. Это важно для того, чтобы ранее загруженные данные были мгновенно доступны при возвращении к месту назначения. Это сложнее сделать с помощью контейнера состояния, который следует жизненному циклу составного экрана.
- При удалении целевого объекта из стека возврата ViewModel также очищается, что гарантирует автоматическую очистку состояния. Это отличается от отслеживания событий освобождения ресурсов, которые могут происходить по разным причинам, например, при переходе на новый экран, изменении конфигурации или по другим причинам.
- Интеграция с другими библиотеками Jetpack, такими как Hilt .
Логика пользовательского интерфейса и её владелец состояния
Логика пользовательского интерфейса — это логика, которая работает с данными, предоставляемыми самим пользовательским интерфейсом. Это может быть состояние элементов пользовательского интерфейса или источники данных пользовательского интерфейса, такие как API разрешений или Resources . Владельцы состояния, использующие логику пользовательского интерфейса, обычно имеют следующие свойства:
- Создаёт состояние пользовательского интерфейса и управляет состоянием его элементов .
- Не сохраняется при повторном создании
Activity: Состояния, хранящиеся в логике пользовательского интерфейса, часто зависят от источников данных самого пользовательского интерфейса, и попытка сохранить эту информацию при изменении конфигурации чаще всего приводит к утечке памяти. Если состояниям необходимо сохранять данные при изменении конфигурации, им следует делегировать задачу другому компоненту, лучше подходящему для сохранения состояния при повторном созданииActivity. Например, в Jetpack Compose состояния элементов пользовательского интерфейса Composable, созданные с помощью функцийremembered, часто делегируют задачу функцииrememberSaveableдля сохранения состояния при повторном созданииActivity. Примерами таких функций являютсяrememberScaffoldState()иrememberLazyListState(). - Содержит ссылки на источники данных, ограниченные областью действия пользовательского интерфейса : к таким источникам данных, как API жизненного цикла и ресурсы, можно безопасно обращаться и считывать, поскольку владелец состояния логики пользовательского интерфейса имеет тот же жизненный цикл, что и сам пользовательский интерфейс.
- Возможность повторного использования в различных пользовательских интерфейсах : разные экземпляры одного и того же элемента управления состоянием логики пользовательского интерфейса могут быть повторно использованы в разных частях приложения. Например, элемент управления состоянием для обработки событий ввода пользователя для группы чипов может использоваться на странице поиска для фильтрующих чипов, а также для поля «Кому» для получателей электронных писем.
Обычно для создания состояния логики пользовательского интерфейса используется простой класс. Это связано с тем, что сам пользовательский интерфейс отвечает за его создание, и это состояние имеет тот же жизненный цикл, что и сам пользовательский интерфейс. Например, в Jetpack Compose состояние является частью композиции и следует жизненному циклу композиции.
Вышеизложенное можно проиллюстрировать на следующем примере из демонстрационного приложения Now in Android :

В примере приложения Now для Android в зависимости от размера экрана устройства отображается либо нижняя панель приложений, либо боковая панель навигации. На небольших экранах используется нижняя панель приложений, а на больших — боковая панель навигации.
Поскольку логика определения подходящего элемента навигации пользовательского интерфейса, используемого в составной функции NiaApp не зависит от бизнес-логики, ею можно управлять с помощью простого класса-хранилища состояния, называемого NiaAppState :
@Stable
class NiaAppState(
val navController: NavHostController,
val windowSizeClass: WindowSizeClass
) {
// UI logic
val shouldShowBottomBar: Boolean
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
// UI logic
val shouldShowNavRail: Boolean
get() = !shouldShowBottomBar
// UI State
val currentDestination: NavDestination?
@Composable get() = navController
.currentBackStackEntryAsState().value?.destination
// UI logic
fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }
/* ... */
}
В приведенном выше примере следует отметить следующие детали, касающиеся NiaAppState :
- Состояние
NiaAppStateне сохраняется при повторном созданииActivity: оноrememberedв Composition путем создания с помощью функцииrememberNiaAppStateиз Composable в соответствии с соглашениями об именовании Compose. После повторного созданияActivityпредыдущий экземпляр теряется, и создается новый экземпляр со всеми его зависимостями, соответствующими новой конфигурации повторно созданнойActivity. Эти зависимости могут быть новыми или восстановленными из предыдущей конфигурации. Например,rememberNavController()используется в конструктореNiaAppStateи делегирует вызов функцииrememberSaveableдля сохранения состояния при повторном созданииActivity. - Содержит ссылки на источники данных, ограниченные областью видимости пользовательского интерфейса : ссылки на
navigationController,Resourcesи другие подобные типы, ограниченные областью видимости жизненного цикла, можно безопасно хранить вNiaAppState, поскольку они имеют общую область видимости жизненного цикла.
Выберите между ViewModel и обычным классом для хранения состояния.
Из предыдущих разделов следует, что выбор между ViewModel и простым контейнером состояния класса сводится к логике, применяемой к состоянию пользовательского интерфейса, и источникам данных, с которыми эта логика работает.
В итоге, на следующей диаграмме показано положение владельцев состояний в производственном конвейере состояний пользовательского интерфейса:

В конечном итоге, состояние пользовательского интерфейса следует создавать с помощью хранителей состояния, расположенных ближе всего к месту его использования . Проще говоря, состояние следует хранить как можно ниже, сохраняя при этом надлежащее право собственности. Если вам нужен доступ к бизнес-логике и состояние пользовательского интерфейса должно сохраняться до тех пор, пока можно перейти на тот или иной экран, даже при повторном создании Activity , ViewModel — отличный выбор для реализации хранителя состояния для бизнес-логики. Для более короткоживущего состояния и логики пользовательского интерфейса достаточно простого класса, жизненный цикл которого зависит исключительно от пользовательского интерфейса.
Владельцы государственных облигаций подлежат начислению сложных процентов.
Владельцы состояний могут зависеть от других владельцев состояний до тех пор, пока срок их зависимости равен или короче. Примерами этого являются:
- Состояние, определяющее логику пользовательского интерфейса, может зависеть от состояния, определяющего логику другого элемента пользовательского интерфейса.
- Состояние на уровне экрана может зависеть от состояния, содержащего логику пользовательского интерфейса.
Следующий фрагмент кода показывает, как DrawerState в Compose зависит от другого внутреннего хранилища состояния, SwipeableState , и как хранилище состояния логики пользовательского интерфейса приложения может зависеть от DrawerState :
@Stable
class DrawerState(/* ... */) {
internal val swipeableState = SwipeableState(/* ... */)
// ...
}
@Stable
class MyAppState(
private val drawerState: DrawerState,
private val navController: NavHostController
) { /* ... */ }
@Composable
fun rememberMyAppState(
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
MyAppState(drawerState, navController)
}
Примером зависимости, которая существует дольше, чем состояние-хранилище, может служить состояние логики пользовательского интерфейса, зависящее от состояния на уровне экрана. Это снизит возможность повторного использования более недолговечного хранилища состояний и предоставит ему доступ к большему количеству логики и состояний, чем ему фактически необходимо.
Если кратковременному хранилищу состояний требуется определенная информация от хранилища состояний более высокого уровня, передавайте в качестве параметра только необходимую информацию, а не экземпляр хранилища состояний. Например, в следующем фрагменте кода класс хранилища состояний логики пользовательского интерфейса получает в качестве параметров только то, что ему необходимо, от ViewModel, вместо того, чтобы передавать весь экземпляр ViewModel в качестве зависимости.
class MyScreenViewModel(/* ... */) {
val uiState: StateFlow<MyScreenUiState> = /* ... */
fun doSomething() { /* ... */ }
fun doAnotherThing() { /* ... */ }
// ...
}
@Stable
class MyScreenState(
// DO NOT pass a ViewModel instance to a plain state holder class
// private val viewModel: MyScreenViewModel,
// Instead, pass only what it needs as a dependency
private val someState: StateFlow<SomeState>,
private val doSomething: () -> Unit,
// Other UI-scoped types
private val scaffoldState: ScaffoldState
) {
/* ... */
}
@Composable
fun rememberMyScreenState(
someState: StateFlow<SomeState>,
doSomething: () -> Unit,
scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
MyScreenState(someState, doSomething, scaffoldState)
}
@Composable
fun MyScreen(
modifier: Modifier = Modifier,
viewModel: MyScreenViewModel = viewModel(),
state: MyScreenState = rememberMyScreenState(
someState = viewModel.uiState.map { it.toSomeState() },
doSomething = viewModel::doSomething
),
// ...
) {
/* ... */
}
На следующей диаграмме показаны зависимости между пользовательским интерфейсом и различными элементами состояния из предыдущего фрагмента кода:

Образцы
Приведенные ниже примеры от Google демонстрируют использование элементов, определяющих состояние (state holders), на уровне пользовательского интерфейса. Изучите их, чтобы увидеть применение этого подхода на практике:
Рекомендуем вам
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- слой пользовательского интерфейса
- Производство состояния пользовательского интерфейса
- Руководство по архитектуре приложений