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 используются аргументы по умолчанию, и мы рекомендуем поступать так же с создаваемыми вами функциями. Это позволяет настраивать компоненты, но при этом легко вызывать их поведение по умолчанию. Например, вы можете создать простой текстовый элемент следующим образом:
Text(text = "Hello, Android!")
Этот код имеет тот же эффект, что и следующий, более подробный код, в котором больше параметров Text заданы явно:
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
Первый фрагмент кода не только проще и понятнее, но и содержит собственную документацию. Если вы укажете только параметр text, это будет означать, что для всех остальных параметров вы хотите использовать значения по умолчанию. Второй фрагмент кода подразумевает, что вы хотите явно задать значения для этих параметров, хотя заданные вами значения являются значениями по умолчанию для функции.
Функции высшего порядка и лямбда-выражения
Kotlin поддерживает функции высшего порядка – функции, которые принимают другие функции в качестве параметров. Compose основан на этом подходе. Например, composable-функция 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(). Подробную информацию можно найти в документации по классам данных.
Объекты Singleton
В Kotlin можно объявлять синглтоны – классы, у которых всегда есть только один экземпляр. Для объявления одноэлементных наборов используется ключевое слово object.
Compose часто использует такие объекты. Например, MaterialTheme определяется как объект-одиночка, а свойства MaterialTheme.colors, shapes и typography содержат значения для текущей темы.
Безопасные по типам конструкторы и DSL
Kotlin позволяет создавать языки, ориентированные на определенную область (DSL), с безопасными по типам конструкторами. DSL позволяют создавать сложные иерархические структуры данных, которые проще поддерживать и читать.
В Jetpack Compose для некоторых API, например LazyRow и LazyColumn, используются DSL.
@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 suspend. Ниже приведен пример использования API ScrollState animateScrollTo.
// 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() } } ) { /* ... */ }
По умолчанию код в сопрограммах выполняется последовательно. Выполняющаяся сопрограмма, которая вызывает функцию приостановки, приостанавливает свое выполнение, пока функция приостановки не вернет значение. Это верно, даже если функция suspend переносит выполнение в другой 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 с 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
- Побочные эффекты в Compose
- Основные сведения о макетах в Compose