Первый экран Compose
Зачем это нужно
Три прошлые главы учили языку Kotlin — переменным, функциям, классам, коллекциям — но ни разу не рисовали ничего на экране телефона. Jetpack Compose — это библиотека от Google, на которой в Android пишут интерфейс (UI, user interface — то, что видит и трогает пользователь): кнопки, тексты, списки, экраны целиком. Пишут его точно на том же Kotlin, но с особыми функциями и правилами, которые эта глава разбирает с нуля.
Главная идея Compose называется декларативным подходом (declarative — «описательный»): вместо того чтобы вручную командовать «найди кнопку и поставь ей новый текст», ты один раз описываешь, как должен выглядеть экран при определённых данных, а Compose сам перерисовывает нужные куски, когда данные меняются. Звучит абстрактно — по ходу главы станет конкретно, на реальном коде.
Здесь же — важное техническое предупреждение. Compose — это не просто Kotlin: чтобы его код заработал, нужен Android-компилятор, эмулятор или телефон, ни того ни другого в браузере нет. Поэтому весь Compose-код в этой главе — статичные блоки с разбором построчно, без кнопки Run: это тот же принцип, что и с MainActivity.kt в главе Фундамента «Android Studio: знакомство с IDE». Зато там, где под капотом Compose скрывается обычный Kotlin — аннотации, лямбды-параметры, цепочки вызовов, — глава даёт исполняемые мини-примеры на чистом Kotlin: их можно запускать прямо здесь и видеть настоящий результат.
- Зачем это нужно
- Аннотация: что такое @Composable
- Text: простейший composable
- Column и Row: раскладка элементов
- Button и обработчик нажатия
- Modifier: цепочка вызовов
- Preview: экран без эмулятора
- Собираем первый экран целиком
- Типичные ошибки
- Слова главы
- Потренируйся печатать
- Символы и приёмы главы
- Куда дальше: проверенные ресурсы
- Челлендж ⭐
- Что должен уметь
Как это было
Аннотация: что такое @Composable
Аннотация вообще — что это за штука
Аннотация (annotation) — это пометка над объявлением (функцией, классом, параметром), которая сама по себе ничего не делает, а несёт метаданные — «информацию о коде для того, кто эту информацию прочитает»: компилятора, IDE или отдельного инструмента. Пишется со знака @ перед именем. Обычная функция выполняет действия; аннотация ничего не выполняет — она просто прикреплена к объявлению как стикер с инструкцией.
Разберём самую важную для этой главы аннотацию по знакам.
Новый символ — @ (собака, at-sign): открывает аннотацию. После неё сразу идёт имя аннотации без пробела, а сама аннотация ставится строго перед тем, что помечает.
Аннотация в чистом Kotlin: что происходит без Compose
Прежде чем нырять в Compose, стоит увидеть, что аннотации существуют и работают в обычном Kotlin — совершенно без Android. Ниже — своя, самодельная аннотация Important. Запусти код: он выполнится, как будто аннотации там вообще нет.
annotation class Important
@Important
fun criticalTask() {
println("Выполняю важную задачу")
}
fun main() {
criticalTask()
}
Разбор по строкам
- Строка 1:
annotation class Important— объявление новой аннотации: ключевые словаannotation classвместо привычногоclassговорят компилятору, что это не обычный класс для создания объектов, а класс-пометка. - Строка 3:
@Important— применяем аннотацию к функции ниже. Синтаксис — тот же@плюс имя, что и в@Composable. - Строки 4–6:
fun criticalTask() { ... }— обычная функция без параметров, печатает строку. - Строка 9:
criticalTask()— обычный вызов функции по имени.
Что выведет: Выполняю важную задачу — и ровно один раз. Аннотация @Important никак не повлияла на выполнение: сама по себе она инертна, это просто метка. Если бы в проекте не было ничего, что читает аннотацию Important (например, кода на рефлексии — механизме, который во время выполнения программы «заглядывает» в такие метки), эффекта не было бы вообще никакого.
Вот в чём вся суть @Composable. Это не волшебное слово, а такая же аннотация — просто её читает не обычная программа, а специальный плагин компилятора Kotlin, который поставляется вместе с Compose (Compose compiler plugin). Он видит @Composable над функцией и переписывает её особым образом: добавляет скрытые механизмы отслеживания, какие данные функция использовала, чтобы при изменении этих данных перерисовать именно нужный кусок экрана, а не всё приложение целиком. Без этого плагина @Composable осталась бы такой же неактивной меткой, как @Important выше.
Что требует @Composable от функции
Официальная документация Compose перечисляет свойства, которых Compose-компилятор ждёт от функции, помеченной @Composable:
- Быстрая и идемпотентная (idempotent) — при одинаковых входных данных функция должна вести себя одинаково, сколько бы раз её ни вызвали.
- Без побочных эффектов (side-effect free) — функция описывает интерфейс и не должна незаметно менять что-то постороннее: глобальную переменную, файл, сетевой запрос.
- Ничего не возвращает — composable-функция не строит виджет и не отдаёт его как результат через
return; вместо этого она сама, по ходу выполнения, описывает, что должно появиться на экране. - Может звать только другие composable-функции — обычная функция вызвать
TextилиButtonне может: отсюда и первая типичная ошибка этой главы, разберём её ниже.
Text: простейший composable
Самая простая composable-функция во всей библиотеке — Text: она рисует текст на экране, примерно как println печатает текст в консоль, только результат появляется не в терминале, а в интерфейсе приложения.
@Composable
fun Greeting() {
Text(text = "Привет, Android!")
}
Этот код не запускается в песочнице Kotlin (как KotlinPlay выше) — Text и аннотация @Composable в её рабочем виде существуют только внутри Android-проекта с подключённой библиотекой Compose, поэтому его можно только прочитать и разобрать здесь, а увидеть в деле — в Android Studio на панели Preview (до неё дойдём в этой же главе).
Разбор по строкам
- Строка 1:
@Composable— та самая аннотация, разобранная выше: без неё Compose-компилятор откажется собирать функцию как часть интерфейса. - Строка 2:
fun Greeting() {— обычное объявление функции без параметров. По соглашению composable-функции называют с заглавной буквы (Greeting, а неgreeting) — так их сразу отличают от обычных функций при чтении кода. - Строка 3:
Text(text = "Привет, Android!")— вызов встроенной composable-функцииText. Параметрtextпередан по имени (именованный аргумент — та же техника, что в главе 2 для вызовов вродеdescribe(name = "ПГК", score = 100)и для функций с параметрами по умолчанию). - Строка 4:
}— конец телаGreeting.
Что должно появиться: на экране приложения — строка Привет, Android!. В отличие от println, у Text нет «вывода в консоль»: результат её работы — это визуальный элемент интерфейса.
Разберём вызов Text по частям — обрати внимание, что здесь ровно тот же принцип именованного аргумента text = ..., что и в describe(name = "ПГК", score = 100) из главы 2 про именованные аргументы.
Column и Row: раскладка элементов
Одного текста мало — экран обычно состоит из нескольких элементов, расположенных друг под другом или в ряд. За это отвечают два контейнера: Column (колонка) складывает содержимое сверху вниз, Row (ряд) — выстраивает слева направо.
Как контейнер получает содержимое: trailing lambda
Column и Row не принимают список готовых элементов как аргумент — вместо этого им передают лямбду (тот самый trailing lambda синтаксис из главы про функции), внутри которой просто вызывают нужные composable-функции одну за другой. Прежде чем смотреть на настоящий Column, повтори этот приём на чистом Kotlin — синтаксис будет идентичным.
fun myColumn(content: () -> Unit) {
println("Колонка началась")
content()
println("Колонка закончилась")
}
fun main() {
myColumn {
println("Текст 1")
println("Текст 2")
}
}
Разбор по строкам
- Строки 1–5:
fun myColumn(content: () -> Unit) { ... }— своя функция-контейнер. Параметрcontentимеет тип() -> Unit— функция без аргументов, ничего не возвращающая (уже знакомый тип из главы про функции и лямбды).myColumnпечатает рамку «до» и «после», а между ними вызываетcontent()— то есть выполняет переданный код ровно там, где сама решит. - Строки 8–11:
myColumn { ... }— вызов с трейлинг-лямбдой: фигурные скобки послеmyColumn— это и есть аргументcontent, вынесенный за круглые скобки (в данном случае скобки вообще можно не писать — синтаксис Kotlin это разрешает, когда лямбда — последний и единственный аргумент).
Что выведет: Колонка началась, Текст 1, Текст 2, Колонка закончилась — по порядку. myColumn сама решает, когда выполнить переданный код (content()), и может обернуть его чем угодно до и после — ровно так же настоящий Column решает, как разложить переданные элементы, прежде чем и после того, как их отрисовать.
Теперь настоящий Column и Row — с той же трейлинг-лямбдой, но уже с composable-функциями внутри:
@Composable
fun ProfileCard(name: String, status: String) {
Column {
Text(text = name)
Text(text = status)
}
}
@Composable
fun StatsRow(wins: Int, losses: Int) {
Row {
Text(text = "Победы: $wins")
Text(text = "Поражения: $losses")
}
}
Разбор по строкам
Column { Text(...); Text(...) }— внутри трейлинг-лямбды два вызоваTextодин за другим; Compose разместит их друг под другом, сверху вниз, в порядке вызова.Row { Text(...); Text(...) }— та же идея, но элементы лягут в ряд, слева направо."Победы: $wins"— знакомый строковый шаблон$из первой главы: подставляет значение параметраwinsпрямо в текст.
Заметь: Column и Row сами по себе не рисуют ничего, кроме порядка — они контейнеры-организаторы. Всё видимое содержимое всегда рисуют элементы внутри них, как Text.
Button и обработчик нажатия
Кнопка — это composable-функция Button, но, в отличие от Text, ей обязательно нужно сказать, что делать при нажатии. За это отвечает параметр onClick — лямбда-функция без аргументов, которую Compose вызовет сам в момент клика.
Событие как лямбда-параметр: подготовка на чистом Kotlin
fun myButton(label: String, onClick: () -> Unit) {
println("[Кнопка: $label]")
onClick()
}
fun main() {
var count = 0
myButton("Нажми меня") {
count++
println("Нажатий: $count")
}
}
Разбор по строкам
- Строка 1:
fun myButton(label: String, onClick: () -> Unit) {— два параметра:label— обычная строка (подпись на кнопке),onClick— снова функция без аргументов и без возврата, ровно какcontentв примере сmyColumn, только по смыслу это не «содержимое», а «что сделать при нажатии». - Строка 2:
println("[Кнопка: $label]")— здесь имитируется отрисовка кнопки на экране: печатаем, как она выглядит. - Строка 3:
onClick()— вызываем переданную лямбду. В реальном Compose этот вызов происходит не сразу, как здесь, а в момент, когда пользователь физически нажмёт на кнопку пальцем — но с точки зрения кода это тот же самый вызов лямбды по имени параметра. - Строка 7:
var count = 0— переменная-счётчик, специальноvar, потому что будет меняться. - Строки 8–11:
myButton("Нажми меня") { count++; println(...) }— вызов с двумя аргументами: обычным"Нажми меня"в скобках и трейлинг-лямбдой дляonClickза скобками.
Что выведет: [Кнопка: Нажми меня], затем Нажатий: 1 — потому что myButton сразу же (в этом упрощённом демо, без реального ожидания клика) печатает кнопку и тут же дёргает onClick(), которая увеличивает count на единицу и печатает результат.
Настоящий Button в Compose устроен по такому же принципу — только вызов onClick откладывается до реального нажатия:
@Composable
fun StartButton(onStartClick: () -> Unit) {
Button(onClick = onStartClick) {
Text(text = "Начать")
}
}
Обрати внимание: у Button фактически два места для кода — именованный аргумент onClick = { ... } внутри скобок (что делать при нажатии) и трейлинг-лямбда после скобок (что нарисовать внутри самой кнопки). Спутать их — частая путаница у новичков: onClick никогда не рисует ничего на экране, он только реагирует на клик.
Text, Column, Row и Button теперь в руках — проверь, что узнаёшь каждый фрагмент по результату, который он нарисует на экране.
Modifier: цепочка вызовов
Modifier — это объект, которым украшают и настраивают composable-функции: задают размер, отступы, реакцию на нажатие, прокрутку и многое другое. Его почти всегда передают как параметр modifier, и вызовы на нём выстраивают в цепочку через точку — тот же принцип .метод().метод(), что уже был в первой главе с listOf(1, 2).map { it * 2 }.sum().
Иммутабельность цепочки: подготовка на чистом Kotlin
Ключевая деталь настоящего Modifier: он неизменяемый (immutable). Каждый вызов на цепочке не меняет существующий модификатор, а создаёт новый, добавляя к нему ещё одно свойство. Повтори этот же принцип на самодельном классе, прежде чем смотреть на настоящий Modifier.
class Box(val width: Int, val height: Int, val padding: Int = 0) {
fun withPadding(dp: Int): Box = Box(width, height, dp)
override fun toString() = "Box(width=$width, height=$height, padding=$padding)"
}
fun main() {
val original = Box(100, 100)
val padded = original.withPadding(16)
println(original)
println(padded)
}
Разбор по строкам
- Строка 1:
class Box(val width: Int, val height: Int, val padding: Int = 0) {— класс с тремя свойствами в основном конструкторе (тема из главы про классы), уpadding— значение по умолчанию0. - Строка 2:
fun withPadding(dp: Int): Box = Box(width, height, dp)— метод не меняетthis(объект, у которого его вызвали), а создаёт и возвращает совершенно новыйBoxс тем жеwidthиheight, но новымpadding. Тело-выражение через=— из главы про функции. - Строка 3:
override fun toString() = ...— переопределённый методtoString, чтобыprintlnпечатал понятный текст, а не служебный адрес объекта в памяти (эта деталь тоже разбиралась в главе про классы). - Строки 7–8:
val original = Box(100, 100)иval padded = original.withPadding(16)— создаём исходныйBox, затем вызываем.withPadding(16)— но результат кладём в НОВУЮ переменнуюpadded, а не переприсваиваемoriginal.
Что выведет: Box(width=100, height=100, padding=0), затем Box(width=100, height=100, padding=16) — два РАЗНЫХ объекта. original не изменился, хотя мы вызвали метод у него: withPadding вернул новый Box, а старый остался нетронутым. Именно так устроен настоящий Modifier.padding(...) — и любой другой метод модификатора: цепочка не редактирует объект «на месте», а на каждом шаге строит новый.
Разбор цепочки посимвольно: Modifier.fillMaxSize().padding(16.dp)
Теперь настоящая цепочка модификаторов — именно та, что чаще всего открывает первый экран Compose: заполнить весь экран и отступить от края.
Новый символ — .dp: точка здесь работает не как вызов метода у объекта, а как обращение к свойству-расширению (extension property) числа 16 — специальному механизму Kotlin, который позволяет «пристроить» новое свойство к уже существующему типу (Int), не переписывая его исходный код. Отсюда и типичная ошибка ниже: если .dp забыть, 16 останется обычным Int, а padding ждёт именно Dp.
Порядок модификаторов имеет значение
Модификаторы применяются строго по порядку слева направо, и от этого порядка зависит итоговое поведение — это не тонкость для продвинутых, а базовое правило, которое стоит усвоить сразу. Официальная документация Compose показывает это на примере модификатора .clickable(onClick = ...) (делает элемент кликабельным) в паре с .padding(...):
- если
.clickable(...)стоит перед.padding(...)— кликабельной становится вся область, включая отступ; - если
.clickable(...)стоит после.padding(...)— область отступа в клик не попадает, реагирует только внутренняя часть.
Каждый модификатор «оборачивает» результат предыдущего, как слои: Modifier.a().b() значит «сначала оберни элемент в a, потом результат — в b», и от этого порядка меняется итоговая форма и поведение, совсем как в цепочке .withPadding(...) из демо выше, где порядок вызовов определял, какой объект получится на выходе.
Правило про порядок легче всего увидеть глазами: переставляй карточки модификаторов и смотри, как меняется тот же самый элемент.
.background(Color.Green)закрасить область цветом.padding(16.dp)отступ 16dp по краям.fillMaxWidth()занять всю доступную ширинуModifier.background(Color.Green).padding(16.dp).fillMaxWidth()background стоит раньше padding: фон закрашивается первым и покрывает всю область элемента, а отступ прижимает текст внутрь цветной зоны.
padding раньше fillMaxWidth: отступ применился первым, элемент заполняет ширину, оставшуюся после него.
Preview: экран без эмулятора
Собирать проект и запускать эмулятор ради каждой мелкой правки — долго. Аннотация @Preview решает эту проблему: она просит Android Studio нарисовать composable-функцию прямо в редакторе, без сборки всего приложения и без эмулятора.
@Composable
fun Greeting(name: String) {
Text(text = "Привет, $name!")
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Greeting(name = "Олег")
}
Разбор по строкам
- Строки 1–4:
Greeting(name: String)— обычная composable-функция с параметром: она ничего не знает заранее про конкретное имя, ждёт его снаружи. - Строка 6:
@Preview(showBackground = true)— говорит Android Studio нарисовать функцию ниже в панели Preview с белым фоном. - Строки 8–10:
fun GreetingPreview() { Greeting(name = "Олег") }— важная деталь: уGreetingPreviewнет параметров. Preview-функции вызывают настоящую composable-функцию с конкретными «тестовыми» данными изнутри, а не принимают их снаружи — иначе Android Studio просто неоткуда взять эти данные для рисования.
Это правило — не параметров у самой Preview-функции — специально проверяется инструментами Compose, и нарушение даёт реальную ошибку, разобранную в следующем разделе.
Собираем первый экран целиком
Сложим всё из этой главы в один настоящий экран: Column с цепочкой Modifier, два Text, Button с onClick, и @Preview для проверки без эмулятора.
@Composable
fun WelcomeScreen(name: String, onStartClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Привет, $name!")
Text(text = "Готов к тренировке?")
Button(onClick = onStartClick) {
Text(text = "Начать")
}
}
}
@Preview(showBackground = true)
@Composable
fun WelcomeScreenPreview() {
WelcomeScreen(name = "Олег", onStartClick = {})
}
Импорты (например, androidx.compose.material3.Button или androidx.compose.ui.Modifier) здесь не показаны — Android Studio добавляет их сама через автодополнение, ровно как это уже было с MainActivity.kt в главе Фундамента про Android Studio.
Разбор по строкам
- Строка 1:
@Composable— без этой аннотации ничего внутри функции — ниColumn, ниText, ниButton— вызвать было бы нельзя: они сами composable и требуют composable-контекста. - Строка 2:
fun WelcomeScreen(name: String, onStartClick: () -> Unit) {— два параметра: обычная строкаnameи лямбда-обработчикonStartClick, которую экран не выполняет сам, а просто передаёт дальше вButton. - Строки 3–7:
Column(modifier = ..., horizontalAlignment = ...) {— уColumnдва именованных аргумента:modifier— та самая цепочкаfillMaxSize().padding(16.dp), растянутая на несколько строк (перенос строки после каждой точки — только для читаемости, компилятору безразличен), иhorizontalAlignment = Alignment.CenterHorizontally— «выровнять всё содержимое по центру по горизонтали». - Строка 9:
Text(text = "Привет, $name!")— первая строка экрана, с уже знакомым строковым шаблоном$name. - Строка 10:
Text(text = "Готов к тренировке?")— вторая строка, фиксированный текст без подстановки. - Строки 11–13:
Button(onClick = onStartClick) { Text(text = "Начать") }— кнопка:onClickполучает лямбду прямо из параметра функции (её код определит тот, кто вызоветWelcomeScreen), а внутри трейлинг-лямбды — надпись на самой кнопке. - Строки 19–21:
WelcomeScreenPreview— вызываетWelcomeScreenс конкретными данными: именем"Олег"и пустой лямбдой{}для клика (в Preview нажать всё равно нельзя, поэтому реальная логика клика не нужна).
Что должно появиться в Preview: экран на весь белый фон предпросмотра, с отступом от края в 16dp, посередине по горизонтали — строка Привет, Олег!, под ней Готов к тренировке?, под ними кнопка с надписью Начать.
Column(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally) {
Text("Привет, Олег!", fontSize = 20.sp)
Text("Готов к тренировке?", fontSize = 16.sp)
Button(onClick = { }) { Text("Начать") }
}Как это устроено под капотом
Типичные ошибки
Пять ошибок, с которыми сталкивается почти каждый на первом Compose-экране. Три из них — про чистый Kotlin под капотом Compose, их можно воспроизвести и увидеть по-настоящему прямо здесь; две — про правила самого Compose, которые проверяются инструментами Android Studio и в браузерной песочнице не воспроизводятся, поэтому показаны статичным кодом с точной формулировкой.
1. Composable-функция вызвана не из composable-контекста
fun onCreate() {
Text(text = "Привет!")
}
@Composable invocations can only happen from the context of a @Composable function
Перевод: «вызовы @Composable-функций возможны только из контекста @Composable-функции». onCreate — обычная функция без аннотации @Composable, а Text — composable; правило из раздела про аннотацию («composable может звать только другие composable») здесь нарушено буквально.
Как починить: пометить вызывающую функцию аннотацией @Composable — либо, если это действительно onCreate экрана (activity), вызвать Text не напрямую, а внутри setContent { ... }, как было показано в главе Фундамента про Android Studio.
2. У Button не хватает onClick
fun myButton(label: String, onClick: () -> Unit) {
println("[Кнопка: $label]")
onClick()
}
fun main() {
myButton("Нажми меня")
}
No value passed for parameter 'onClick'.
Перевод: «не передано значение для параметра onClick». У myButton (а в реальном Compose — у Button) параметр onClick не имеет значения по умолчанию, поэтому его нельзя пропустить при вызове — ровно та же логика, что и с любой другой обязательной функцией из главы про функции и параметры по умолчанию.
Как починить: добавить лямбду: myButton("Нажми меня") { println("клик") }. В реальном коде — Button(onClick = { /* что делать */ }) { Text("Начать") }.
3. Число вместо Dp в отступе
Text(
text = "Привет, Android!",
modifier = Modifier.padding(16)
)
Argument type mismatch: actual type is 'Int', but 'Dp' was expected.
Перевод: «несовпадение типа аргумента: фактический тип Int, а ожидался Dp». padding в реальном Compose принимает значение типа Dp, а не голое число; 16 без .dp остаётся обычным Int — то же самое несовпадение типов, что уже разбиралось в главе про переменные, только там оно было между Int и String, а здесь — между Int и Dp. Формулировка сообщения проверена вживую в этой сессии на минимальном классе Dp, устроенном так же, как настоящий (value class Dp(val value: Float)) — механизм проверки типов в компиляторе один и тот же независимо от того, откуда взят класс Dp.
Как починить: дописать .dp: Modifier.padding(16.dp).
4. У Preview-функции остался обязательный параметр
@Preview
@Composable
fun GreetingPreview(name: String) {
Greeting(name = name)
}
Composable functions with non-default parameters are not supported in Preview unless they are annotated with @PreviewParameter.
Перевод: «composable-функции с параметрами без значения по умолчанию не поддерживаются в Preview, если не помечены аннотацией @PreviewParameter». Android Studio не может сама придумать значение для name — Preview просто не соберёт данные для рисования.
Как починить: убрать параметр и передать конкретное значение внутри, как в рабочем примере этой главы (fun GreetingPreview() { Greeting(name = "Олег") }), либо, если действительно нужно несколько вариантов превью с разными данными, использовать @PreviewParameter с отдельным поставщиком значений — тема для более глубокого погружения.
5. Не закрыта фигурная скобка в лямбде-контейнере
fun myColumn(content: () -> Unit) {
println("Колонка началась")
content()
println("Колонка закончилась")
}
fun main() {
myColumn {
println("Текст 1")
println("Текст 2")
}
Syntax error: Expecting '}'.
Перевод: «синтаксическая ошибка: ожидалась }». У трейлинг-лямбды после myColumn не хватает закрывающей скобки — а значит, не хватает и закрывающей скобки у самой функции main: компилятор дочитал файл до конца и не нашёл нужное количество }.
Как починить: досчитать скобки и закрыть обе: сначала лямбду, потом main. В реальном Compose эта же ошибка чаще всего вылезает, когда в глубоко вложенных Column { Row { Button { ... } } } теряется одна из скобок где-то в середине.
Слова главы
Потренируйся печатать
Сигнатура composable-функции с аннотацией — то, что придётся набирать перед каждым экраном:
Цель: скорость ≥ 100 зн/мин, точность ≥ 90%
@Composable fun Greeting(name: String) {
Цепочка модификаторов — самая частая связка символов на первом экране:
Цель: скорость ≥ 120 зн/мин, точность ≥ 90%
Modifier.fillMaxSize().padding(16.dp)
Символы и приёмы главы
| Символ / приём | Как называется | Что делает | Пример |
|---|---|---|---|
@ | аннотация (собака) | помечает объявление метаданными для компилятора или инструментов IDE | @Composable |
.dp | свойство-расширение | превращает число в единицу измерения Dp, не зависящую от плотности экрана | 16.dp |
Modifier.a().b() | цепочка модификаторов | каждый вызов оборачивает результат предыдущего в новый неизменяемый Modifier | Modifier.fillMaxSize().padding(16.dp) |
param = value | именованный аргумент | передаёт значение в параметр функции по имени, а не по позиции | Text(text = "Привет") |
{ ... } после скобок | трейлинг-лямбда | передаёт код как последний аргумент функции — так контейнеры получают содержимое | Column { Text("A") } |
onClick = { ... } | лямбда-обработчик события | код, который выполнится при нажатии — вызывается не сразу, а по клику | Button(onClick = { count++ }) |
showBackground = true | именованный Boolean-аргумент | включает непрозрачный фон в панели Preview | @Preview(showBackground = true) |
() -> Unit | тип функции без результата | тип лямбды-параметра вроде content или onClick — ничего не принимает и не возвращает | content: () -> Unit |
Углубиться
Что такое рекомпозиция на самом деле. Compose декларативен: единственный способ обновить интерфейс — вызвать ту же composable-функцию заново с новыми аргументами. Каждый раз, когда меняются данные, которые функция использовала (например, значение, обёрнутое в remember { mutableStateOf(...) } — механизм хранения состояния, отдельная большая тема), Compose запускает рекомпозицию (recomposition): выполняет функцию заново и обновляет на экране только то, что реально изменилось, а не весь экран целиком. Именно ради возможности эффективно отследить, какие данные функция использовала, и существует плагин компилятора, который распознаёт @Composable.
Фазы работы Compose. Под капотом путь от composable-функции до пикселей на экране состоит из трёх фаз: composition (какие элементы вообще нужны и в каком порядке — то, что описывает сама функция), layout (сколько места каждый элемент займёт и где будет расположен — здесь работает Modifier) и drawing (собственно отрисовка пикселей). Понимание этого разделения объясняет, например, почему порядок модификаторов в цепочке влияет именно на layout-фазу: каждый модификатор в цепочке — это ещё один слой измерения и расположения, обёрнутый вокруг предыдущего.
Дальше по теме:
- Composable functions and the mental model — официальное объяснение декларативного подхода и требований к composable-функциям.
- Thinking in Compose: state and composition — про remember, mutableStateOf и то, как рекомпозиция реагирует на изменение состояния.
Куда дальше: проверенные ресурсы
- Хабр — Библиотека «Jetpack Compose» для начинающего Android-разработчика — открой, если хочется того же материала другими словами: аннотация, Modifier, базовые элементы и раскладки разобраны с нуля на русском.
- Jetpack Compose: быстрый старт и пример первого экрана — открой, чтобы увидеть первый экран целиком в контексте настоящего проекта: с настройкой Gradle и более сложными элементами вроде списка и плавающей кнопки.
- Урок 5: Column & Row — позиционирование — открой для более подробного разбора выравнивания внутри Column и Row на практическом примере шапки приложения.
-
Layouts basics — Column, Row, Box — открой за исходным источником примеров этой главы и разделом про Box — третий контейнер, который здесь не разбирался.
-
Compose modifiers — открой за полным списком готовых модификаторов и более подробным объяснением, почему их порядок важен.
-
Preview your UI with composable previews — открой за подробностями про
@PreviewParameterи предпросмотр в разных темах и размерах экрана. -
Jetpack Compose basics (codelab) — открой для пошагового практикума с заданиями: тот же материал, но с реальным проектом, анимацией и списком из тысячи элементов.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Челлендж ⭐
Экран WelcomeScreen из середины главы уже собран. Дальше — без запуска эмулятора, только чтением и написанием кода (как и с MainActivity.kt в главе Фундамента про Android Studio): реши обе ступени на бумаге или в текстовом редакторе, а потом свериcь с решением.
Ступень 1. Под кнопкой «Начать» нужно добавить ещё одну строку текста — версию приложения: Text(text = "Версия 1.0"). Перепиши тело Column { ... } из WelcomeScreen так, чтобы эта строка оказалась самой нижней на экране.
Ступень 2 ⭐. В WelcomeScreen модификатор устроен так: Modifier.fillMaxSize().padding(16.dp). Представь, что вместо этого в цепочку добавили .clickable(onClick = onStartClick) — сначала в самое начало (Modifier.clickable(onClick = onStartClick).fillMaxSize().padding(16.dp)), а потом попробовали в самый конец (Modifier.fillMaxSize().padding(16.dp).clickable(onClick = onStartClick)). Объясни своими словами, чем будет отличаться поведение этих двух вариантов — какая область экрана будет реагировать на нажатие в каждом случае.
Решение (сначала попробуй сам)
Ступень 1:
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Привет, $name!")
Text(text = "Готов к тренировке?")
Button(onClick = onStartClick) {
Text(text = "Начать")
}
Text(text = "Версия 1.0")
}
Строку Text(text = "Версия 1.0") достаточно добавить последним вызовом внутри Column { ... } — порядок вызовов внутри трейлинг-лямбды и есть порядок элементов на экране сверху вниз.
Ступень 2 ⭐:
Когда .clickable(...) стоит первым (Modifier.clickable(...).fillMaxSize().padding(16.dp)), кликабельной становится вся область, которую в итоге займёт Column после fillMaxSize и padding, — включая отступ в 16dp по краям: clickable в этом случае оборачивает ещё «пустой» модификатор, а размер и отступ добавляются уже поверх уже кликабельной области.
Когда .clickable(...) стоит последним (Modifier.fillMaxSize().padding(16.dp).clickable(...)), к моменту его применения размер и отступ уже вычтены — кликабельной окажется только внутренняя часть, без 16dp отступа по краям.
Что должен уметь
- Объяснять, что такое аннотация вообще, и почему
@Composableсама по себе ничего не делает без плагина компилятора Compose. - Читать и писать простейшие composable-функции с
Text,Column,Row,Button. - Узнавать трейлинг-лямбду как способ передать содержимое контейнеру и отличать её от именованного лямбда-параметра вроде
onClick. - Строить и читать цепочку
Modifier, объясняя, что делает каждое звено, и предсказывать, как порядок модификаторов меняет поведение. - Объяснять назначение
@Previewи почему у Preview-функции не может быть параметра без значения по умолчанию. - Собирать простой экран из
Column+Text+Button+ цепочкиModifierи читать его построчно. - Узнавать по сообщению компилятора или панели Preview пять типичных ошибок этой темы — вызов composable вне контекста, отсутствующий
onClick, число вместоDp, параметр у Preview-функции, незакрытую скобку — и чинить каждую. - Объяснять разницу между Kotlin-кодом, который можно запустить прямо в браузере (KotlinPlay), и Compose-кодом, для которого нужен полноценный Android-проект.
Комментарии
Комментарии появятся после настройки. Нужен аккаунт GitHub — вход прямо в виджете выше.