В 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) в зависимости от того, как долго вам нужно хранить значение.
Значение компонуемого объекта TextField имеет тип String , поэтому его можно получить откуда угодно: из жёстко заданного значения, из ViewModel или передать из родительского компонуемого объекта. Хранить его в объекте State не обязательно, но необходимо обновить значение при вызове onValueChange .
Определить компонуемые параметры
При определении параметров состояния компонуемого объекта помните о следующих вопросах:
- Насколько многоразовым или гибким является композируемый материал?
- Как параметры состояния влияют на производительность этого составного объекта?
Чтобы способствовать разделению и повторному использованию, каждый компонуемый элемент должен содержать как можно меньше информации. Например, при создании компонуемого элемента для заголовка новостной статьи предпочтительнее передавать только ту информацию, которая должна быть отображена, а не всю новостную статью целиком:
@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 должна обрабатывать их и обновлять состояние пользовательского интерфейса.
Уровень пользовательского интерфейса никогда не должен изменять свое состояние вне обработчика событий, поскольку это может привести к несоответствиям и ошибкам в вашем приложении.
Предпочитайте передачу неизменяемых значений для лямбда-выражений состояний и обработчиков событий. Такой подход имеет следующие преимущества:
- Вы улучшаете возможность повторного использования.
- Вы проверяете, что ваш пользовательский интерфейс не изменяет значение состояния напрямую.
- Вы избегаете проблем с параллелизмом, поскольку гарантируете, что состояние не будет изменено из другого потока.
- Зачастую вы уменьшаете сложность кода.
Например, компонуемый объект, принимающий String и лямбда-выражение в качестве параметров, может вызываться из множества контекстов и обладает высокой степенью повторного использования. Предположим, что верхняя панель приложения всегда отображает текст и имеет кнопку «Назад». Вы можете определить более универсальный компонуемый объект 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 ) } }, // ... ) }
ViewModels, состояния и события: пример
Используя 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 // ... }
В дополнение к 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 отключен.
- State и Jetpack Compose
- Сохранение состояния пользовательского интерфейса в Compose
- Обработка пользовательского ввода