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

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

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

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

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

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

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

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

Рисунок 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.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, обратитесь к следующим ресурсам:

Образцы

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