Jetpack Compose основан на Kotlin. В некоторых случаях Kotlin предоставляет специальные идиомы, которые упрощают написание хорошего кода Compose. Если вы думаете на другом языке программирования и мысленно переводите его на Kotlin, вы, скорее всего, упустите некоторые преимущества Compose и вам будет трудно понять идиоматический код Kotlin. Чтобы избежать этих проблем, лучше познакомьтесь со стилем Kotlin.
Аргументы по умолчанию
При написании функции Kotlin вы можете указать значения по умолчанию для аргументов функции, которые будут использоваться, если вызывающий объект не передает эти значения явным образом. Эта функция снижает потребность в перегруженных функциях.
Например, вы хотите написать функцию, которая рисует квадрат. У этой функции может быть один обязательный параметр sideLength, указывающий длину каждой стороны. У нее может быть несколько необязательных параметров, например thickness, edgeColor и т. д. Если вызывающий объект не указывает их, функция использует значения по умолчанию. В других языках вам, возможно, пришлось бы написать несколько функций:
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
В Kotlin можно написать одну функцию и указать значения по умолчанию для аргументов:
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
Это не только избавляет от необходимости писать несколько избыточных функций, но и делает код более понятным. Если вызывающий абонент не указывает значение аргумента, это означает, что он готов использовать значение по умолчанию. Кроме того, именованные параметры позволяют лучше понять, что происходит. Если вы посмотрите на код и увидите вызов функции, подобный этому, вы можете не знать, что означают параметры, не проверив код drawSquare():
drawSquare(30, 5, Color.Red);
А вот пример самодокументируемого кода:
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
В большинстве библиотек Compose используются аргументы по умолчанию. Рекомендуем делать то же самое для создаваемых вами composable-функций. Это позволяет настраивать composable-функции, но при этом упрощает вызов поведения по умолчанию. Например, вы можете создать простой текстовый элемент:
Text(text = "Hello, Android!")
Этот код дает тот же результат, что и приведенный ниже более подробный код, в котором явно задано больше параметров Text:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
Первый фрагмент кода не только проще и понятнее, но и содержит встроенную документацию. Указав только параметр text, вы подтверждаете, что для всех остальных параметров хотите использовать значения по умолчанию. Второй фрагмент кода, напротив, подразумевает, что вы хотите явно задать значения для других параметров, хотя заданные вами значения являются значениями по умолчанию для функции.
Функции высшего порядка и лямбда-выражения
Kotlin поддерживает функции высшего порядка, которые принимают другие функции в качестве параметров. Compose основан на этом подходе. Например, компонуемая функция Button предоставляет лямбда-параметр onClick. Значение этого параметра – функция, которая вызывается при нажатии кнопки.
Button( // ... onClick = myClickFunction ) // ...
Функции высшего порядка естественным образом сочетаются с лямбда-выражениями, которые вычисляются как функция. Если функция нужна вам только один раз, вам не нужно определять её в другом месте, чтобы передать её функции высшего порядка. Вместо этого вы можете просто определить функцию прямо там с помощью лямбда-выражения.
В предыдущем примере предполагается, что myClickFunction() определен в другом месте.
Но если вы используете эту функцию только здесь, проще определить ее с помощью лямбда-выражения:
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
Замыкающие лямбда-выражения
В Kotlin есть специальный синтаксис для вызова функций высшего порядка, у которых последний параметр является лямбда-выражением. Если вы хотите передать лямбда-выражение в качестве этого параметра, вы можете использовать синтаксис завершающей лямбда-функции. Вместо того, чтобы помещать лямбда-выражение в круглые скобки, вы помещаете его после них. Это обычная ситуация в Compose, поэтому вам нужно знать, как выглядит код.
Например, последним параметром для всех макетов, таких как composable-функция Column(), является content – функция, которая создает дочерние элементы интерфейса. Предположим, вы хотите создать столбец, содержащий
три текстовых элемента, и вам нужно применить некоторое форматирование. Этот код будет работать, но он очень громоздкий:
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
Поскольку параметр content является последним в сигнатуре функции и мы передаем его значение в виде лямбда-выражения, мы можем вынести его за скобки:
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
Оба примера имеют абсолютно одинаковое значение. Фигурные скобки определяют лямбда-выражение, которое передается в параметр content.
Если лямбда-выражение – это единственный параметр, который вы передаете, то есть если последний параметр является лямбда-выражением и вы не передаете никаких других параметров, то круглые скобки можно вообще опустить. Например, предположим, что вам не нужно передавать модификатор в Column. Вы можете написать код следующим образом:
Column { Text("Some text") Text("Some more text") Text("Last text") }
Этот синтаксис довольно распространен в Compose, особенно для элементов макета, таких как Column. Последний параметр – это лямбда-выражение, определяющее дочерние элементы, которые указываются в фигурных скобках после вызова функции.
Области действия и получатели
Некоторые методы и свойства доступны только в определенной области видимости. Ограниченная область действия позволяет предлагать функции там, где они нужны, и избегать их случайного использования там, где они не подходят.
Рассмотрим пример, используемый в Compose. При вызове composable-функции макета Row лямбда-выражение контента автоматически вызывается в RowScope.
Это позволяет Row предоставлять функции, которые работают только в Row.
В примере ниже показано, как Row предоставляет значение для модификатора align, относящееся к строке.
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
Некоторые API принимают лямбда-выражения, которые вызываются в области получателя. Эти лямбда-выражения имеют доступ к свойствам и функциям, определенным в других местах, на основе объявления параметра:
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
Подробную информацию можно найти в разделе Литералы функций с получателем документации по Kotlin.
Делегированные ресурсы
Kotlin поддерживает делегированные свойства. Эти свойства вызываются так же, как и поля, но их значение определяется динамически путем вычисления выражения. Их можно узнать по синтаксису by:
class DelegatingClass { var name: String by nameGetterFunction() // ... }
Другой код может получить доступ к свойству с помощью следующего кода:
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
При выполнении println() вызывается nameGetterFunction(), чтобы вернуть значение строки.
Эти делегированные свойства особенно полезны при работе со свойствами, поддерживаемыми состоянием:
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
Деструктуризация классов данных
Если вы определите класс данных, то сможете получить доступ к данным с помощью деструктурирующего объявления. Например, вы определяете класс Person:
data class Person(val name: String, val age: Int)
Если у вас есть объект этого типа, вы можете получить доступ к его значениям с помощью кода, подобного этому:
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
Вы часто будете видеть такой код в функциях Compose:
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
Классы данных предоставляют множество других полезных функций. Например, когда вы определяете класс данных, компилятор автоматически определяет полезные функции, такие как equals() и copy(). Подробную информацию можно найти в документации по классам данных.
Объекты-одиночки
Kotlin позволяет объявлять одиночки – классы, у которых всегда есть один и только один экземпляр. Они объявляются с помощью ключевого слова object.
Compose часто использует такие объекты. Например, MaterialTheme определяется как объект-одиночка, а свойства MaterialTheme.colors, shapes и typography содержат значения для текущей темы.
Типобезопасные сборщики и DSL
Kotlin позволяет создавать предметно-ориентированные языки (DSL) с помощью типобезопасных построителей. DSLs allow building complex hierarchical data structures in a more maintainable and readable way.
Jetpack Compose использует DSL для некоторых API, таких как LazyRow и LazyColumn.
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
Kotlin позволяет создавать типобезопасные построители с помощью литералов функций с получателем.
Если взять в качестве примера composable-функцию Canvas, она принимает в качестве параметра функцию с DrawScope в качестве получателя, onDraw: DrawScope.() -> Unit, что позволяет блоку кода вызывать функции-члены, определенные в DrawScope.
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
Подробнее о типобезопасных конструкторах и DSL можно узнать в документации Kotlin.
Сопрограммы Kotlin
Сопрограммы обеспечивают поддержку асинхронного программирования на уровне языка в Kotlin. Сопрограммы могут приостанавливать выполнение, не блокируя потоки. Адаптивный интерфейс по своей природе асинхронен, и Jetpack Compose решает эту проблему, используя сопрограммы на уровне API вместо обратных вызовов.
Jetpack Compose предлагает API, которые делают использование сопрограмм безопасным на уровне пользовательского интерфейса.
Функция rememberCoroutineScope возвращает CoroutineScope, с помощью которой можно создавать сопрограммы в обработчиках событий и вызывать API приостановки Compose. Ниже приведен пример использования API animateScrollTo в ScrollState.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
По умолчанию сопрограммы выполняют блок кода последовательно. Выполняющаяся сопрограмма, которая вызывает функцию приостановки, приостанавливает свое выполнение до тех пор, пока функция приостановки не вернет значение. Это верно, даже если функция приостановки перемещает выполнение в другой CoroutineDispatcher. В предыдущем примере loadData не будет выполнена, пока не вернется функция приостановки animateScrollTo.
Чтобы выполнять код параллельно, необходимо создать новые сопрограммы. В примере выше, чтобы распараллелить прокрутку экрана вверх и загрузку данных из viewModel, нужны две сопрограммы.
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
Сопрограммы упрощают объединение асинхронных API. В следующем примере мы объединяем модификатор pointerInput с Animation API, чтобы анимировать положение элемента, когда пользователь нажимает на экран.
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen and animate // in the same block awaitPointerEventScope { val offset = awaitFirstDown().position // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
Подробнее о сопрограммах рассказывается в руководстве Сопрограммы Kotlin в Android.
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Компоненты и макеты Material Design
- Побочные эффекты в Compose
- Основы макета Compose