В Compose интерфейс неизменяем: его нельзя обновить после отрисовки. Вы можете управлять состоянием интерфейса. Каждый раз, когда состояние интерфейса меняется, Compose воссоздает части дерева интерфейса, которые изменились. Компонуемые функции могут принимать состояние и предоставлять события. Например, функция TextField принимает значение и предоставляет обратный вызов onValueChange, который запрашивает у обработчика обратного вызова изменение значения.
var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } )
Поскольку компонуемые функции принимают состояние и предоставляют события, однонаправленный поток данных хорошо подходит для Jetpack Compose. В этом руководстве рассказывается, как реализовать однонаправленный поток данных в Compose, как использовать события и держатели состояния, а также как работать с ViewModels в Compose.
Однонаправленный поток данных
Однонаправленный поток данных (UDF) – это шаблон проектирования, в котором состояние передается вниз, а события – вверх. Однонаправленный поток данных позволяет отделить компоненты, которые отображают состояние в интерфейсе, от частей приложения, которые хранят и изменяют состояние.
Цикл обновления интерфейса приложения с однонаправленным потоком данных выглядит следующим образом:
- Событие. Элемент интерфейса создает событие и передает его вверх, например нажатие кнопки передается в ViewModel для обработки. Также событие может быть передано из других слоев приложения, например чтобы указать, что срок действия сеанса пользователя истек.
- Обновление состояния. Обработчик событий может изменить состояние.
- Отображение состояния. Держатель состояния передает состояние, и интерфейс его отображает.
Использование этого подхода при работе с Jetpack Compose дает ряд преимуществ:
- Тестируемость. Отделение состояния от интерфейса, в котором оно отображается, упрощает тестирование обоих компонентов.
- Инкапсуляция состояния. Поскольку состояние можно изменить только в одном месте и у каждого компонента есть только один источник данных о состоянии, вероятность возникновения ошибок из-за несогласованности состояний снижается.
- Согласованность интерфейса. Все изменения состояния сразу же отражаются в интерфейсе благодаря использованию наблюдаемых держателей состояния, таких как
StateFlowилиLiveData.
Однонаправленный поток данных в Jetpack Compose
Компонуемые функции работают на основе состояний и событий. Например, значение TextField обновляется только тогда, когда обновляется параметр value и вызывается обратный вызов onValueChange – событие, которое запрашивает изменение значения. Compose определяет объект State как держатель значения, и изменения значения состояния запускают повторную композицию. Состояние можно хранить в remember { mutableStateOf(value) } или rememberSaveable { mutableStateOf(value) } в зависимости от того, как долго вам нужно помнить значение.
Тип значения для composable-функции TextField – String, поэтому оно может быть получено из любого источника: из жестко заданного значения, из ViewModel или передано из родительской composable-функции. Хранить его в объекте State необязательно, но вам нужно обновить значение при вызове onValueChange.
mutableStateOf(value)создаетMutableState– наблюдаемый тип в Compose. Любые изменения его значения приводят к перекомпоновке любых композиционных функций, которые считывают это значение.rememberсохраняет объекты в композиции и забывает об объекте, когда из композиции удаляется composable-функция, вызвавшаяremember.rememberSaveableсохраняет состояние при изменении конфигурации, сохраняя его вBundle.
Как задать параметры, которые можно комбинировать
При определении параметров состояния для 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), будет выполняться повторная композиция, даже если 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. Вот как можно смоделировать состояние экрана и событие:
У экрана четыре состояния:
- Вышел из аккаунта – пользователь ещё не вошел в аккаунт.
- В процессе. Приложение пытается войти в аккаунт пользователя, выполняя сетевой вызов.
- Ошибка – при возникновении ошибки при входе.
- Вошел в аккаунт – когда пользователь вошел в аккаунт.
Эти состояния можно представить в виде запечатанного класса. ViewModel предоставляет статус в виде State, задает начальный статус и обновляет его при необходимости. Тег ViewModel также обрабатывает событие входа, предоставляя метод onSignIn().
class MyViewModel : ViewModel() { private val _uiState = mutableStateOf<UiState>(UiState.SignedOut) val uiState: State<UiState> get() = _uiState // ... }
Помимо mutableStateOf API, 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
- Обработка пользовательского ввода