Прямые ссылки на публичные уроки для быстрого старта и стабильной индексации lesson-страниц.
Почему в Jetpack Compose можно верстать функциями? Android разработка активно опиралась на Java, но постепенно перешла на Kotlin, который сегодня практически полностью вытеснил Java.
Разработчики Kotlin заложили важную особенность языка: возможность опускать последний параметр метода, если он является лямбдой, и записывать его в фигурных скобках.
Рассмотрим пару примеров:
fun main() { fun sayHello(name: String, message: () -> String) { println("Hello, $name! ${message()}") } fun sayBye(name: String, message: () -> String) { println("Bye, $name! ${message()}") } // Вызов без вынесения лямбды sayHello("Alice", {"How are you today?"}) // Вызов с вынесением лямбды sayBye("John") { "See you later!" } }
В этом файле сначала объявлены два метода, где первый принимающий параметр строка, а второй лямбда. Во втором — круглая скобка закрывается сразу, а лямбда выносится наружу. Это позволяет строить более хитрые вложенные конструкции:
sayBye("John") { sayHello("Alice") { "How are you today?" } "See you later!" }
Такая особенность языка позволяет строить Domain-Specific Language на основе Kotlin.
DSL — это когда мы даём разработчику возможность описывать логику или конфигурацию максимально компактным и наглядным образом.
Пример — файлы конфигурации сборки Gradle (с расширением .kts). Они пишутся на Kotlin DSL, где всё является вызовом функций. Можно провалиться в них и посмотреть сигнатуру. Когда мы используем эти функции, они выполняют работу "за кулисами" — нам не нужно знать детали реализации на низком уровне.
Сделаем свой DSL, чтобы понять, как под капотом работает магия конфигурационных файлов.
class GreetingDSL(val mood: String) { fun sayHello(name: String, message: String) { println("[$mood] Hello, $name! $message") } fun sayBye(name: String, message: String) { println("[$mood] Bye, $name! $message") } } fun main() { // Функция для создания DSL fun greeting( mood: String, block: GreetingDSL.() -> Unit ) { val dsl = GreetingDSL(mood) // Передаём настроение в DSL dsl.block() // Выполняем переданный блок в контексте объекта } // !!! Объект класса GreetingDSL НЕ создается явно greeting("Happy") { sayHello("Alice", "How are you today?") } greeting("Serious") { sayBye("Mark", "Let’s talk tomorrow.") } }
Два метода обёрнуты в класс GreetingDSL со свойством mood. Интересное — в функции greeting(): она принимает строку с настроением и лямбду. Лямбда содержит GreetingDSL.() — если видите такую конструкцию, всё внутри этой функции выполняется так, будто мы уже находимся внутри объекта GreetingDSL.
Это значит, что можно вызывать методы и свойства GreetingDSL напрямую, без явного обращения к объекту. При помощи такого подхода можно скрывать несущественные детали реализации внутри класса.
Jetpack Compose — это набор классов и функций, представляющих DSL для описания интерфейсов. Элементы экрана описываются с помощью специфичных для предметной области функций с максимально простым синтаксисом:
Text с параметром text. Composable функции, в отличие от обычного синтаксиса Kotlin, пишутся с большой буквы.Button, передай коллбэк в onClick. В лямбду помести Text — он разместится внутри кнопки. Лямбда выносится наружу, потому что это последний параметр функции Button:Button(onClick = { }) { Text(text = "Button") }
Остальные параметры — цвета, формы, обводка, отступы — легко узнаваемы.
Разработчики Jetpack Compose представили мощный DSL, скрывший всё несущественное для верстки. Использование Compose похоже на сборку конструктора Lego из готовых деталей. Всё, что нужно знать — язык Kotlin. А если понадобится — можно создать собственный "кубик" и использовать его наравне со стандартными.
В Kotlin есть scope-функции (функции области видимости), позволяющие временно "погружаться" в контекст объекта. Стандартные: let, run, with, apply и also.
Коротко о применении:
1. Элегантная инициализация. С помощью apply или also можно настроить объект без промежуточных переменных, обращаясь к полям напрямую через this или к объекту через it:
val person = Person().apply { name = "Alice" age = 20 }
2. Избежание проверки на null. Через let цепочка операций вызывается только если объект не равен null:
val text: String? = "Hello World" text?.let { println(it.length) // Только если text не null }
3. Локальный контекст. with(object) { ... } позволяет не засорять пространство имён и группировать вызовы — мы "заходим" в объект и вызываем его методы напрямую.
Помимо стандартных, ничто не мешает создавать собственные scope-функции для дополнительного контроля контекста.
Compose использует похожий подход "погружения" в контекст, только вместо apply или with — DSL-области (scope): RowScope, ColumnScope и другие.
RowScope — это "комната", где доступны только функции и свойства, связанные с размещением элементов в строку. Когда мы пишем Text и Button внутри Row, мы неявно находимся в контексте RowScope. Не нужно вручную указывать принадлежность — Row сам "открывает" свою область видимости.
Без Compose это выглядело бы так:
val rowScope = RowScope() rowScope.apply { addText("Hello") addButton { addText("Click me!") } }
В Compose — лаконично:
Row { Text("Hello") Button { Text("Click me!") } }
Jetpack Compose — это декларативный UI фреймворк, где интерфейс пишется на Kotlin.
Слово "декларация" означает "объявление". При декларативном подходе мы сразу объявляем в коде то, что хотим получить на экране.
При классическом подходе через XML мы описывали "как" получить результат — такой подход называется императивным (от лат. imperativus — повелительный). Он фокусируется на пошаговом описании процесса: какие действия и в каком порядке выполнить.
Например, чтобы вывести текст через TextView, нужно было:
TextView или создать его программно.LinearLayout, ConstraintLayout и т.д.).В Compose на первый взгляд кажется, что подход тоже императивный — мы же пишем инструкции. Но это декларативный подход — мы декларируем, что:
Greeting — произвольное пользовательское название;Text, отвечающая за отрисовку текста (аналог TextView, но не связана с классом View);setContent — условный аналог setContentView.Не нужно создавать экземпляр View и обращаться к его свойствам. Объявляем Composable функции с нужными параметрами, вызываем в setContent — система отрисовывает результат. Вот что такое настоящая декларация.
В следующем уроке рассмотрим, что такое и как работают Composable функции, а также что происходит внутри setContent и зачем нужен метод Scaffold.