В Compose пользовательский интерфейс неизменяем – его нельзя обновить после отрисовки. Вы можете контролировать состояние пользовательского интерфейса. Каждый раз, когда состояние пользовательского интерфейса меняется, Compose воссоздает его части, которые изменились. Composable-функции могут принимать состояние и предоставлять события. Например, TextField принимает значение и предоставляет обратный вызов onValueChange, который запрашивает у обработчика обратных вызовов изменение значения.
var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } )
Поскольку composable-функции принимают состояние и предоставляют события, шаблон однонаправленного потока данных хорошо подходит для Jetpack Compose. В этом руководстве рассказывается, как реализовать шаблон однонаправленного потока данных в Compose, как реализовать события и хранилища состояний, а также как работать с ViewModel в Compose.
Однонаправленный поток данных
Однонаправленный поток данных (UDF) – это шаблон проектирования, в котором состояние передается вниз, а события – вверх. Следуя однонаправленному потоку данных, вы можете отделить composable-функции, которые отображают состояние в пользовательском интерфейсе, от частей приложения, которые хранят и изменяют состояние.
Цикл обновления пользовательского интерфейса для приложения, использующего однонаправленный поток данных, выглядит следующим образом:
- Событие. Часть пользовательского интерфейса генерирует событие и передает его вверх, например нажатие кнопки передается в ViewModel для обработки. Или событие передается из других слоев вашего приложения, например, указывая на то, что сеанс пользователя истек.
- Обновление состояния. Обработчик событий может изменить состояние.
- Отображение состояния. Хранилище состояния передает состояние, а пользовательский интерфейс отображает его.
Следование этому шаблону при использовании Jetpack Compose дает несколько преимуществ:
- Тестируемость. Отделение состояния от пользовательского интерфейса, который его отображает, упрощает тестирование обоих компонентов по отдельности.
- Инкапсуляция состояния. Поскольку состояние можно обновить только в одном месте и существует только один источник достоверных данных для состояния composable-функции, вероятность возникновения ошибок из-за несогласованных состояний снижается.
- Согласованность интерфейса. Все обновления состояния сразу же отражаются в интерфейсе благодаря использованию наблюдаемых хранилищ состояния, таких как
StateFlowилиLiveData.
Однонаправленный поток данных в Jetpack Compose
Работа composable-функций основана на состояниях и событиях. Например, TextField обновляется только при обновлении параметра value и предоставляет обратный вызов onValueChange – событие, которое запрашивает изменение значения на новое. Compose определяет объект State как держатель значения, и изменения значения состояния запускают рекомпозицию. Вы можете хранить состояние в remember { mutableStateOf(value) } или rememberSaveable { mutableStateOf(value) } в зависимости от того, как долго вам нужно помнить значение.
Тип значения composable-функции TextField – String, поэтому оно может быть получено откуда угодно: из заданного в коде значения, из ViewModel или из родительской composable-функции. Необязательно хранить его в объекте State, но нужно обновлять значение при вызове onValueChange.
mutableStateOf(value)создаетMutableState– наблюдаемый тип в Compose. Любые изменения его значения планируют рекомпозицию любых composable-функций, которые считывают это значение.rememberсохраняет объекты в композиции и забывает объект, когда composable-функция, которая вызвалаremember, удаляется из композиции.rememberSaveableсохраняет состояние при изменении конфигурации, записывая его вBundle.
Определите параметры composable-функции
При определении параметров состояния composable-функции учитывайте следующие вопросы:
- Насколько composable-функция гибкая и подходит для повторного использования?
- Как параметры состояния влияют на производительность этой composable-функции?
Чтобы обеспечить разделение и повторное использование, каждая composable-функция должна содержать как можно меньше информации. Например, при создании composable-функции для заголовка новостной статьи лучше передавать только ту информацию, которую нужно показать, а не всю статью.
@Composable fun Header(title: String, subtitle: String) { // Recomposes when title or subtitle have changed. } @Composable fun Header(news: News) { // Recomposes when a new instance of News is passed in. }
Иногда использование отдельных параметров также повышает производительность. Например, если News содержит не только title и subtitle, то при каждой передаче нового экземпляра News в Header(news) composable-функция будет перекомпонована, даже если title и subtitle не изменились.
Тщательно продумайте, сколько параметров вы передаете. Функция со слишком большим количеством параметров менее удобна, поэтому в этом случае предпочтительно сгруппировать их в класс.
События в Compose
Каждое действие пользователя в приложении должно быть представлено как событие: нажатия, изменения текста и даже таймеры или другие обновления. Поскольку эти события изменяют состояние пользовательского интерфейса, ViewModel должен обрабатывать их и обновлять состояние пользовательского интерфейса.
Слой пользовательского интерфейса никогда не должен менять состояние вне обработчика событий, поскольку это может привести к несоответствиям и ошибкам в вашем приложении.
Предпочитайте передавать неизменяемые значения для лямбда-выражений состояния и обработчика событий. У этого подхода есть следующие преимущества:
- Вы улучшаете возможность повторного использования.
- Вы убеждаетесь, что ваш пользовательский интерфейс не изменяет значение состояния напрямую.
- Вы избегаете проблем с параллелизмом, потому что гарантируете, что состояние не изменяется из другого потока.
- Часто вы уменьшаете сложность кода.
Например, composable-функцию, которая принимает в качестве параметров String и лямбда-выражение, можно вызывать из разных контекстов и использовать многократно. Предположим, что верхняя панель приложения в вашем приложении всегда отображает текст и имеет кнопку «Назад». Вы можете определить более общую composable-функцию MyAppTopAppBar, которая получает текст и обработчик кнопки "Назад" в качестве параметров:
@Composable fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) { TopAppBar( title = { Text( text = topAppBarText, textAlign = TextAlign.Center, modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) }, navigationIcon = { IconButton(onClick = onBackPressed) { Icon( Icons.AutoMirrored.Filled.ArrowBack, contentDescription = localizedString ) } }, // ... ) }
ViewModel, состояния и события: пример
Используя ViewModel и mutableStateOf, вы также можете внедрить в приложение однонаправленный поток данных, если выполняется одно из следующих условий:
- Состояние пользовательского интерфейса отображается с помощью наблюдаемых хранилищ состояния, таких как
StateFlowилиLiveData. ViewModelобрабатывает события, поступающие из пользовательского интерфейса или других уровней приложения, и обновляет хранилище состояния на основе этих событий.
Например, при нажатии кнопки Войти на экране входа в систему в приложении должен отображаться индикатор выполнения и выполняться сетевой вызов. Если вход выполнен успешно, приложение переходит на другой экран, а в случае ошибки – показывает Snackbar. Вот как можно смоделировать состояние экрана и событие:
У экрана есть четыре состояния:
- Выход из аккаунта. Пользователь ещё не вошел в аккаунт.
- In progress (Выполняется). Приложение пытается выполнить вход пользователя, отправляя сетевой вызов.
- Ошибка – при входе произошла ошибка.
- Выполнен вход. Пользователь вошел в аккаунт.
Вы можете смоделировать эти состояния как изолированный класс. ViewModel предоставляет состояние как State, задает начальное состояние и обновляет его по мере необходимости. Класс ViewModel также обрабатывает событие входа, предоставляя метод onSignIn().
class MyViewModel : ViewModel() { private val _uiState = mutableStateOf<UiState>(UiState.SignedOut) val uiState: State<UiState> get() = _uiState // ... }
Помимо API mutableStateOf, Compose предоставляет расширения для LiveData, Flow и Observable, чтобы регистрировать их в качестве слушателей и представлять значение как состояние.
class MyViewModel : ViewModel() { private val _uiState = MutableLiveData<UiState>(UiState.SignedOut) val uiState: LiveData<UiState> get() = _uiState // ... } @Composable fun MyComposable(viewModel: MyViewModel) { val uiState = viewModel.uiState.observeAsState() // ... }
Подробнее…
Дополнительную информацию об архитектуре в Jetpack Compose можно найти в следующих ресурсах:
Образцы
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Состояние и Jetpack Compose
- Сохранение состояния пользовательского интерфейса в Compose
- Обработка пользовательского ввода