Программирование
Концепции Jetpack Compose, которые должен знать каждый разработчик
Эти концепции — это всего лишь введение в то, что может предложить Compose.
Что такое Compose?
Jetpack Compose — это новый декларативный UI Framework для Android. Разработчики Android уже давно привыкли писать пользовательский интерфейс в xml с использованием Stateful Views, которые обновляются пошагово через View Hierarchy. В Jetpack Compose пользовательский интерфейс пишется без сохранения состояния с помощью функций Kotlin.
Составные функции помечаются аннотацией @Composable. Composable функции должны быть так аннотированы, чтобы проинформировать компилятор о том, что эта функция добавляет элемент пользовательского интерфейса в иерархию представлений. Хотя составные функции могут вызывать другие стандартные функции, сами составные компоненты могут вызываться только из других составных компонентов.
Если вы еще этого не сделали, я настоятельно рекомендую вам ознакомиться с планом обучения, предоставленным Google для Compose, поскольку он демонстрирует важные детали и примеры, которые вы можете использовать, чтобы быстро начать работу с Compose.
Однонаправленный поток данных
Compose построен на основе однонаправленного потока данных, и ожидается, что эта парадигма будет соблюдаться в правильной реализации Compose. В отличие от старой UI-системы, Composables должны быть Stateless, не сохранять состояния — это означает, что их отображения должны определяться аргументами, передаваемыми в саму Composable-функцию.
В самом простом смысле это означает, что события, которые происходят либо из пользовательского интерфейса (щелчки кнопок, ввод текста и т.д.), либо из других источников (вызовы API, обратные вызовы и т.д.) обрабатываются обработчиком, который затем, в свою очередь, обновляет состояние пользовательского интерфейса, которое переходит в составные функции. Поскольку Composables не имеют состояния, предоставленное состояние и будет использоваться для создания пользовательского интерфейса.
В приведенном выше потоке слой UI будет вашим Composable. События, происходящие из этого уровня, такие как нажатия кнопок, передаются в обработчик событий, например ViewModel. ViewModel предоставит пользовательскому интерфейсу состояние через LiveData/StateFlow. По мере изменения состояния обновления отправляются в ваши составные объекты, которые затем перекомпоновываются с использованием полученного состояния.
Приведенный ниже код демонстрирует вышеуказанный однонаправленный поток — сбор состояния из ViewModel, отправка событий в ViewModel и обновление состояния ViewModel.
MVI_ComposeTheme { Surface(color = MaterialTheme.colors.background) { val state = viewModel.viewState.collectAsState().value Button(onClick = { viewModel.processEvent()}) { Text(state.message) } } }
Композиция и рекомпозиция
Композиция — это процесс, в котором выполняются ваши составные функции и создается пользовательский интерфейс для пользователя. Рекомпозиция — это процесс обновления пользовательского интерфейса в результате изменения состояния или данных, которое Composable использует для отображения. Во время перекомпоновки Compose может понять, какие данные использует каждый Composable, и обновляет только те компоненты пользовательского интерфейса, которые были изменены. Остальные составные элементы пропускаются.
Композицию/рекомпозицию не следует приравнивать к жизненному циклу.
- Составные функции могут перекомпоновываться чуть ли не каждый кадр (например, в анимации).
- Составные функции могут вызываться в любом порядке.
- Составные функции могут выполняться параллельно.
Это означает, что вы никогда не должны включать логику в выполнение Composable функций.
Compose_Theme { MainScreen() // DO NOT DO THIS viewModel.makeAPICall() }
Composable с отслеживанием состояния и сохранение Instance State — до свидания!
Хотя наша цель состоит в том, чтобы наши Composables в значительной степени не имели состояния, иногда нам нужно, чтобы их части были Stateful — например, чтобы запоминать состояние прокрутки, совместно использовать переменную между Composables и т.д. Так как рекомпозиция может происходить каждый кадр,было бы плохо, если бы пользователь терял свою позицию в прокрутке каждый раз, когда происходит перекомпоновка.
Мы можем сделать это, создав и запомнив переменную в нашем Composable:
val myInt = remember{ Random(10).nextInt() }
В приведенном выше примере случайно сгенерированное целое число будет запоминаться между композициями без пересчета. Если это целое число не было заключено в remember, оно будет пересчитываться при любой перекомпоновке.
Сделав еще один шаг вперед, мы также можем запоминать эти данные между изменениями конфигурации!
val myInt = rememberSaveable{ Random(10).nextInt() }
Иногда нам нужно иметь запоминаемую переменную, которая при обновлении одним Composable вызывает перекомпоновку другого Composable. В приведенном ниже примере нажатие кнопки увеличивает счетчик кликов, этот счетчик используется в текстовом компоненте для отображения. Таким образом, при нажатии кнопки мы хотим, чтобы текст отображался с обновленным счетчиком.
Чтобы сделать это, мы будем использовать функцию запоминания, предоставляемую Compose, как мы делали выше, но Integer будет заключен в объект MutableState. Класс MutableState — это единственное значения, чтение и запись которого отслеживаются Compose и вызовут перекомпоновку затронутых Composables.
Column{ // create state for buttonCount and a function to update it - // setButtonCount val (buttonCount, setButtonCount) = rememberSaveable { mutableStateOf(0) } Button(onClick = { setButtonCount(buttonCount + 1) }) { Text(text = "Press Me!") } // recomposes whenever button is pressed Text(text = "Button Pressed $buttonCount") }
Слоты API
Compose предлагает концепцию Slot API. Это позволяет Composables легко настраивать составные элементы без использования составных функций, обеспечивая бесконечное количество реализаций для различных настроек, которые вы можете применить. Поскольку все варианты использования и реализации могут отличаться, Slot API предоставляют пустые слоты в компонуемом объекте, где может находиться ваш настраиваемый пользовательский интерфейс.
Например, многие кнопки содержат больше, чем просто текст внутри них. Некоторые отображают загрузчики, некоторые отображают значки слева, некоторые — справа. Слоты позволяют вам предоставить свой собственный Composable, который предоставит эти различные варианты кастомизации.
Другие составные элементы, такие как Scaffolds, полностью состоят из слотов. Думайте об этих составных элементах как о схеме того, как будет выглядеть ваш пользовательский интерфейс с зарезервированным пространством для различных компонентов пользовательского интерфейса, таких как Toolbar, BottomNav, Drawer, содержимое экрана и т.д..
Модификаторы
Модификаторы можно сравнить с атрибутами xml, которые вы традиционно используете для стилизации своего пользовательского интерфейса, однако модификаторы намного проще в использовании и имеют еще несколько возможностей.
Modifiers позволяют украсить или изменить реализацию Composable по умолчанию. С их помощью вы можете изменить внешний вид, добавить информацию для доступности, обработать взаимодействие с событиями пользовательского интерфейса и многое другое. Модификаторы — это просто объекты Kotlin, поэтому их также можно добавлять для настраиваемых модификаторов.
Text(text = "Your Text", modifier = Modifier.padding(5.dp))
Модификаторы — мощное средство, поскольку они предоставляют возможность добавлять ваши Composable слои, не вкладывая их в другие составные элементы. Например, приведенный ниже пользовательский интерфейс не может быть реализован в старой системе пользовательского интерфейса Android без вложения нескольких View.
Однако с модификаторами в Compose мы можем добиться этого с помощью всего одного Composable. Это связано с тем, что порядок применения модификаторов имеет значение, и, используя паддинг и раскраску в различном порядке, мы можем достичь множества различных комбинаций в реализации пользовательского интерфейса.
Text(text = "Fake Button", modifier = Modifier.padding(5.dp) .background(Color.Magenta) .padding(5.dp) .background(Color.Yellow))
Ленивые списки
LazyList — это Compose-эквивалент для RecyclerView. У меня не было ни дня, чтобы я не писал адаптеров RecyclerView, ViewHolders и всего другого стандартного кода, который идет вместе с ними. В приведенном ниже примере показан LazyList в форме столбца (вертикальная прокрутка), который показывает различные элементы пользовательского интерфейса. С RecyclerView это означает, что нам понадобится адаптер и как минимум два разных ViewHolder. С Compose нам просто нужна составная функция LazyColumn, которая динамически добавляет контент элементов.
val listSize = 100 LazyColumn { items(listSize) { if (it % 2 == 0) { Text("I am even") }else{ Text("I am odd") } } }
Вот и все. Наверное, это моя любимая вещь в Compose.
Constraint Layout
У Compose есть собственная версия ConstraintLayout, которую мы все знаем и любим по старой системе создания UI. ConstraintLayouts хорошо использовались там, поскольку предоставили возможность создавать настраиваемый пользовательский интерфейс с зависимостями от других представлений без кучи вложенных View. С Compose вложение представлений больше не является проблемой, которая была раньше, но иногда нам все еще нужно использовать такие инструменты, как constraints, barriers, weights и т.д., которые может предложить ConstraintLayout.
ConstraintLayout { // Creates references for the three Composables val (button1, button2, text) = createRefs() Button( // constraintAs is like setting the ID, required. modifier = Modifier.constrainAs(button1) { top.linkTo(parent.top, margin = 16.dp) } ) { Text("Button 1") } // constraintAs is like setting the ID, required. Text("Text", Modifier.constrainAs(text) { top.linkTo(button1.bottom, margin = 16.dp) centerAround(button1.end) }) // Create barrier to set the right button to the right of the button or the text, which ever is longer. val barrier = createEndBarrier(button1, text) Button( modifier = Modifier.constrainAs(button2) { top.linkTo(parent.top, margin = 16.dp) start.linkTo(barrier) } ) { Text("Button 2") } }
Compose и Навигация
Навигация в Jetpack Compose может использовать многие функции, к которым мы привыкли с Jetpack Navigation. Однако с Jetpack Compose теперь у нас есть возможность перемещаться между экранами с помощью одного Activity без необходимости использовать фрагменты.
Просто создайте NavHost с вложенным в него Composable-компонентом Screen.
val navController = rememberNavController() NavHost(navController = navController, startDestination = "home") { composable("home"){ val vm: HomeVM = viewModel() HomeScreen(vm) } composable("settings"){ val vm: SettingsVM = viewModel() SettingsScreen(vm) } composable("profile"){ val vm: ProfileVM = viewModel() ProfileScreen(vm) } }
С помощью ndroidx.lifecycle:lifecycle-viewmodel-compose вы можете создавать ViewModels в своих Composables:
val vm: MyVM = viewModel()
ViewModels, созданные в Composables, будут сохраняться до тех пор, пока их область действия (Activity/Fragment) не будет уничтожена. Это позволяет вам иметь ViewModels для ваших экранов NavHost так же, как вы это делали бы с Фрагментами. В моем примере у меня есть ViewModels, созданные в NavHost, это просто пример, чтобы вы могли увидеть их использование.
Чтобы применить ваш NavHost к BottomNav или другому компоненту навигации, используйте Scaffold:
Scaffold( bottomBar = { BottomNavigation { // your navigation composable here } }, ) { NavHost( navController = navController, startDestination = "home") { // your screen composables } }
Итого
Вышеупомянутые концепции — это всего лишь введение в то, что может предложить Compose. Compose — это полный сдвиг в том, как разработчики Android создают пользовательский интерфейс, но это долгожданное изменение, которое значительно упрощает решение многих проблем предыдущей UI-системы. Если вы еще этого не сделали, я настоятельно рекомендую вам ознакомиться с учебным курсом, предусмотренным для Compose. Это довольно длинный путь, но каждый его шаг стоит потраченного времени. Удачных композиций!