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

В Compose пользовательский интерфейс неизменяем — его невозможно обновить после отрисовки. Вы можете управлять только состоянием пользовательского интерфейса. Каждый раз, когда состояние пользовательского интерфейса изменяется, Compose воссоздаёт изменившиеся части дерева пользовательского интерфейса . Компонуемые элементы могут принимать состояние и предоставлять события — например, 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

Компонуемые объекты работают на основе состояния и событий. Например, 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, обратитесь к следующим ресурсам:

Образцы

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