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

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

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

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

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

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

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

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

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

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

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

Композируемые объекты работают на основе состояния и событий. Например, 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 , то всякий раз, когда в Header(news) передается новый экземпляр News , компонуемый объект будет пересобран, даже если title и subtitle не изменились.

Тщательно обдумайте количество передаваемых параметров. Функция со слишком большим количеством параметров ухудшает её эргономику, поэтому в данном случае предпочтительнее сгруппировать их в класс.

События в композиции

Каждый ввод данных в ваше приложение должен быть представлен как событие: касания, изменения текста и даже таймеры или другие обновления. По мере того, как эти события изменяют состояние вашего пользовательского интерфейса, 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, states и events: пример

Используя ViewModel и mutableStateOf , вы также можете обеспечить однонаправленный поток данных в вашем приложении, если выполняется одно из следующих условий:

  • Состояние вашего пользовательского интерфейса предоставляется с помощью наблюдаемых объектов состояния, таких как StateFlow или LiveData .
  • ViewModel обрабатывает события, поступающие из пользовательского интерфейса или других слоев вашего приложения, и обновляет содержимое stateholder в зависимости от этих событий.

Например, при реализации экрана входа в систему, нажатие на кнопку «Войти» должно приводить к отображению индикатора выполнения и сообщения о сетевом запросе. Если вход в систему прошел успешно, приложение переходит на другой экран; в случае ошибки приложение отображает 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, обратитесь к следующим ресурсам:

Образцы

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}