В этой третьей части мы рассмотрим контекстные приемники Kotlin и то, как они могут нам помочь, прежде чем мы начнем собирать все вместе в части 4 и закончим работу над слайдером Labeled Range в части 5.
Сначала давайте посмотрим, что такое приемник контекста, прежде чем мы рассмотрим, как он может помочь нам сделать наш компонент более удобным в использовании.
Важно: Kotlin Context Receiver является экспериментальным API, это означает, что он может быть изменен в будущем, до выхода финального релиза. Кроме того, поддержка IDE в настоящее время также ограничена.
Включение контекстного приемника
Из-за экспериментального характера контекстных приемников, они не включены по умолчанию. Чтобы включить их использование, нам нужно перейти к файлу build.gradle.kts или build.gradle нашего модуля и добавить -Xcontext-receivers в качестве свободного аргумента компилятора.
В файле build.gradle модуля Android это выглядит примерно так:
android {
...
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ["-Xcontext-receivers"]
}
}
Как это работает
С помощью приемников контекста мы можем добавить один или несколько контекстов в наши функции или методы. Это обеспечивает функциональность объявленного контекста в нашей функции, как если бы в этой функции было еще одно this. Затем мы можем вызывать методы этого контекста, как будто наша функция является частью объекта.
На первый взгляд это немного похоже на функции расширения, такие как
fun String.toast(activity: Activity, duration: Int = Toast.LENGTH_LONG) {
Toast.makeText(activity, this, duration).show()
}
В этой функции видно, что для вызова функции Toast.makeText нам также нужна Android Activity, поэтому нам нужно вызвать функцию следующим образом:
"Hello Toast".toast(this@MainActivity)
Мы знаем, что хотим вызывать эту функцию только из Android Activity, поскольку она показывает элемент пользовательского интерфейса. Что мы можем сделать с помощью Context Receiver, мы можем объявить нашу функцию тоста вызываемой в области действия Android Activity.
context(Activity)
fun String.toast(duration: Int = Toast.LENGTH_LONG) {
Toast.makeText(this@Activity, this, duration).show()
}
Как вы можете видеть, мы убрали параметр Activity и вместо него добавили Context Receiver, заявив, что эта функция действительна в области действия Activity. Это позволяет нам вызывать тосты без непосредственной передачи Activity, если мы находимся в области действия Activity.
"Hello Toast".toast()
Кроме того, наша функция теперь сообщает членам нашей команды, что эту функцию можно вызывать только в Activity, а не, например, в фоновой службе.
Возникает вопрос: что происходит, когда мы не находимся в области действия объекта, необходимого в качестве приемника контекста?
В качестве примера, давайте изменим нашу функцию тоста так, чтобы в качестве получателей контекста были Активность и Строка
context(Activity, String)
fun toast(duration: Int = Toast.LENGTH_LONG) {
Toast.makeText(this@Activity, this@String, duration).show()
}
Теперь нам нужно, чтобы наша строка также находилась в области действия. Мы можем добиться этого с помощью функции with языка Kotlin.
with("Hello Context") {
toast()
}
Конечно, это немного странный пример, но он демонстрирует, как мы можем создать Scope, а также как использовать несколько приемников контекста :-).
Как это нам поможет?
Для нашего слайдера Labeled Range, приемники контекста могут помочь нам сделать наш API чище и проще в использовании как для вызывающего пользователя, так и для нас.
Начнем с простого: мы хотим нарисовать текст на холсте и дать пользователю возможность настроить размер текста, а также горизонтальный и вертикальный перевод позиционирования. В нашем Composable нам нужны пиксельные значения для позиционирования и рисования текста. Но мы не хотим, чтобы вызывающие наш Composable самостоятельно вычисляли значения пикселей, и предлагаем им API, который можно использовать со значениями Dp и/или Sp.
Для этого мы создаем класс конфигурации TextConfig, содержащий настраиваемые свойства и задающий им некоторые значения по умолчанию.
class TextConfig(
...
private val verticalTranslation: Dp = 0.dp,
private val horizontalTranslation: Dp = 0.dp,
private val size: TextUnit = 8.sp,
...
)
Чтобы иметь возможность конвертировать значения Dp или Sp в пиксели, мы можем использовать функцию toPx, как мы уже видели во второй части этой серии. Функция toPx доступна только тогда, когда мы находимся в пределах DrawScope холста. Поэтому мы можем либо вызывать toPx вручную каждый раз в нашем холсте, либо использовать контекстные приемники и создать красивые вычисляемые свойства, что упрощает использование.
class TextConfig(
...
private val verticalTranslation: Dp = 0.dp,
private val horizontalTranslation: Dp = 0.dp,
private val size: TextUnit = 8.sp,
...
) {
context(DrawScope)
val sizePx: Float
get() = size.toPx()
context(DrawScope)
val verticalTranslationPx: Float
get() = verticalTranslation.toPx()
context(DrawScope)
val horizontalTranslationPx: Float
get() = horizontalTranslation.toPx()
}
Таким образом, мы можем реализовать наш Composable следующим образом:
@Composable
fun DrawText(
textConfig: TextConfig
) {
var enteredText by remember { mutableStateOf("") }
Column(
modifier = Modifier.padding(16.dp)
) {
TextField(
value = enteredText,
onValueChange = { enteredText = it }
)
Canvas(
modifier = Modifier
.fillMaxSize()
.background(color = Color.DarkGray)
) {
val paint = Paint().apply {
textSize = textConfig.sizePx
}
val x = (size.width / 2f) - (enteredText.length / 4f * textConfig.sizePx) + textConfig.horizontalTranslationPx
val y = size.height / 2f + textConfig.sizePx / 2f + textConfig.verticalTranslationPx
drawIntoCanvas {
it.nativeCanvas.drawText(
enteredText,
x,
y,
paint
)
}
}
}
}
Мы видим, что пока мы находимся в области видимости Canvas, мы можем вызывать наши свойства, ограниченные DrawScope, потому что лямбда Canvas предоставляет DrawScope в качестве параметра функции расширения и вот так.
Пользователь Composable может предоставить свою конфигурацию с независимыми от устройства значениями.
val config = TextConfig(
size = 32.sp,
verticalTranslation = 16.dp,
horizontalTranslation = 32.dp,
)
DrawText(
textConfig = config
)
Заключение
С помощью контекстных приемников мы можем предоставить хороший API нашим пользователям, а также облегчить себе жизнь при реализации нашего Composable.
Весь представленный здесь код и немного больше можно найти на GitHub.
Теперь, когда мы знаем, как рисовать элементы на холсте, взаимодействовать с ними с помощью распознавания жестов, а также умеем создавать красивый простой API, у нас все готово для реализации нашего Labeled Range Slider.
Сразу же переходим к четвертой части