Архитектура пользовательского интерфейса Compose

В Compose UI неизменяем — нет способа обновить его после того, как он был отрисован. Вы можете контролировать состояние своего UI. Каждый раз, когда состояние UI изменяется, Compose воссоздает измененные части дерева UI . Компонуемые элементы могут принимать состояние и предоставлять события — например, TextField принимает значение и предоставляет обратный вызов onValueChange , который запрашивает обработчик обратного вызова для изменения значения.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Поскольку компонуемые объекты принимают состояние и выставляют события, шаблон однонаправленного потока данных хорошо подходит для Jetpack Compose. В этом руководстве основное внимание уделяется тому, как реализовать шаблон однонаправленного потока данных в Compose, как реализовать события и держатели состояний, а также как работать с ViewModels в Compose.

Однонаправленный поток данных

Однонаправленный поток данных (UDF) — это шаблон проектирования, в котором состояние течет вниз, а события — вверх. Следуя однонаправленному потоку данных, вы можете отделить компонуемые элементы, которые отображают состояние в пользовательском интерфейсе, от частей вашего приложения, которые хранят и изменяют состояние.

Цикл обновления пользовательского интерфейса для приложения, использующего однонаправленный поток данных, выглядит следующим образом:

  1. Событие : часть пользовательского интерфейса генерирует событие и передает его наверх, например, нажатие кнопки передается в ViewModel для обработки; или событие передается из других слоев вашего приложения, например, указание на то, что сеанс пользователя истек.
  2. Обновление состояния : обработчик событий может изменить состояние.
  3. Отображение состояния : держатель состояния передает состояние, и пользовательский интерфейс отображает его.

События передаются от пользовательского интерфейса к держателю состояния, а состояние передается от держателя состояния к пользовательскому интерфейсу.
Рисунок 1. Однонаправленный поток данных.

Следование этой схеме при использовании Jetpack Compose дает ряд преимуществ:

  • Тестируемость : отделение состояния от пользовательского интерфейса, который его отображает, упрощает тестирование обоих по отдельности.
  • Инкапсуляция состояний : поскольку состояние может быть обновлено только в одном месте и существует только один источник истины для состояния составного объекта, вероятность возникновения ошибок из-за несогласованных состояний снижается.
  • Согласованность пользовательского интерфейса : все обновления состояний немедленно отражаются в пользовательском интерфейсе с помощью наблюдаемых держателей состояний, таких как StateFlow или LiveData .

Однонаправленный поток данных в Jetpack Compose

Composables работают на основе состояния и событий. Например, TextField обновляется только тогда, когда обновляется его параметр value и он выставляет обратный вызов onValueChange — событие, которое запрашивает изменение значения на новое. Compose определяет объект State как держатель значения, а изменения значения состояния запускают перекомпозицию. Вы можете удерживать состояние в remember { mutableStateOf(value) } или rememberSaveable { mutableStateOf(value) в зависимости от того, как долго вам нужно помнить значение.

Тип значения компонуемого TextFieldString , поэтому он может поступать откуда угодно — из жестко закодированного значения, из 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, обратитесь к следующим ресурсам:

Образцы

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}