Перейти к основному содержимому
04МОБИЛКА · ГЛАВА 04Первый экран Compose

Первый экран Compose

Прочитано 0%Квизы 0/5Тренажёры 0/5Уровень 1 · Новичок

Зачем это нужно

Три прошлые главы учили языку 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: их можно запускать прямо здесь и видеть настоящий результат.

Как это было
Jetpack Compose анонсировали на конференции Google I/O в мае 2019 года. После десяти лет XML-вёрсток Google решила начать с чистого листа и построить UI декларативно — вдохновившись подходом React и Flutter: описываешь, «что показать», а не «как перестроить экран». До стабильной версии 1.0 дозревали два года — она вышла в июле 2021-го, и с тех пор Compose — рекомендуемый Google способ строить интерфейсы Android.

Аннотация: что такое @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 не может: отсюда и первая типичная ошибка этой главы, разберём её ниже.
Отвечено вопросов: 0 из 4
Что такое аннотация (annotation) сама по себе, без какого-либо инструмента, который её читает?
Почему @Composable вообще на что-то влияет, если аннотации сами по себе инертны?
Какое из требований НЕ относится к тому, что Compose ждёт от composable-функции?
Можно ли вызвать функцию, помеченную @Important из демо-примера, вообще без слова @Important перед ней?
Отвечено верно: 0 из 4

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 (ряд) — выстраивает слева направо.

Column { }Row { }Box { }
Три базовых контейнера Compose: Column складывает сверху вниз, Row — слева направо, Box накладывает элементы слоями (встретится дальше в курсе).

Как контейнер получает содержимое: 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.

Отвечено вопросов: 0 из 4
Что стоит внутри фигурных скобок после Column в примере Column { Text(...); Text(...) }?
В демо myColumn { println("Текст 1"); println("Текст 2") } — почему строки "Колонка началась" и "Колонка закончилась" в принципе появляются в выводе?
Чем Row отличается от Column?
Что выведет println("Победы: $wins"), если wins = 7?
Отвечено верно: 0 из 4

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 никогда не рисует ничего на экране, он только реагирует на клик.

Отвечено вопросов: 0 из 4
В myButton(label: String, onClick: () -> Unit) — какой тип у параметра onClick?
Чем именованный аргумент onClick = onStartClick отличается от трейлинг-лямбды после Button(...) { Text("Начать") }?
В демо myButton("Нажми меня") { count++; println("Нажатий: $count") } — когда именно выполнится count++?
Почему в реальном Compose Button(onClick = { ... }) не печатает результат сразу же, как это делает myButton в демо-примере?
Отвечено верно: 0 из 4

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(...) — область отступа в клик не попадает, реагирует только внутренняя часть.
COMPOSE · ПОРЯДОК МОДИФИКАТОРОВ МЕНЯЕТ РЕЗУЛЬТАТ.clickable().padding(16.dp)клик считается и на отступе — рамка целиком.padding(16.dp).clickable()клик на отступе не срабатывает — рамка только внутри
Порядок модификаторов меняет результат: clickable перед padding делает кликабельным всё вместе с отступом, после — только внутреннюю часть.
Важно

Каждый модификатор «оборачивает» результат предыдущего, как слои: Modifier.a().b() значит «сначала оберни элемент в a, потом результат — в b», и от этого порядка меняется итоговая форма и поведение, совсем как в цепочке .withPadding(...) из демо выше, где порядок вызовов определял, какой объект получится на выходе.

Правило про порядок легче всего увидеть глазами: переставляй карточки модификаторов и смотри, как меняется тот же самый элемент.

Задание 1 из 2: Сделай отступ прозрачным: фон НЕ должен закрашивать зону padding — цветная область только внутри отступа.
.background(Color.Green)закрасить область цветом
.padding(16.dp)отступ 16dp по краям
.fillMaxWidth()занять всю доступную ширину
Привет, Compose!
Modifier.background(Color.Green).padding(16.dp).fillMaxWidth()

background стоит раньше padding: фон закрашивается первым и покрывает всю область элемента, а отступ прижимает текст внутрь цветной зоны.

padding раньше fillMaxWidth: отступ применился первым, элемент заполняет ширину, оставшуюся после него.

Отвечено вопросов: 0 из 4
В демо с Box — почему println(original) после вызова original.withPadding(16) всё ещё печатает padding=0?
Что означает точка между fillMaxSize() и padding(16.dp) в цепочке Modifier.fillMaxSize().padding(16.dp)?
Почему Modifier.padding(16) (без .dp) — ошибка, а Modifier.padding(16.dp) — нет?
Если поменять местами .clickable(onClick = ...) и .padding(...) в цепочке — что изменится?
Отвечено верно: 0 из 4

Preview: экран без эмулятора

Собирать проект и запускать эмулятор ради каждой мелкой правки — долго. Аннотация @Preview решает эту проблему: она просит Android Studio нарисовать composable-функцию прямо в редакторе, без сборки всего приложения и без эмулятора.

COMPOSE · @Preview БЕЗ ЭМУЛЯТОРА@Composablefun Greeting(name: String) {Text("Привет, $name!")}@Preview(showBackground = true)@Composableбез сборкиGreetingPreviewПривет, Олег!Android Studio рисует composable прямо в IDE — эмулятор для этого не нужен
@Preview рисует composable-функцию прямо в IDE — ни сборки всего приложения, ни эмулятора не нужно.
@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.

COMPOSE · ДЕРЕВО ВЫЗОВОВ WelcomeScreenWelcomeScreenColumnTextTextButtonTextкаждый вызов в трейлинг-лямбде — ветка: Button сам вызывает свой Text
Дерево вызовов: WelcomeScreen вызывает Column, а тот — Text, Text и Button; Button вызывает свой собственный Text.

Разбор по строкам

  • Строка 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("Начать") }
}
Параметры — превью и код меняются вместе
Column · arrangement
Измени параметры 3 раза и посмотри, как меняется код: 0 из 3
Как это устроено под капотом
Телефон слева — это не Android и не Compose, а обычный HTML: вёрстка описана JSON-деревом, где Column и Row превращаются во flexbox-контейнеры (flex-direction: column / row), Box — в CSS grid с наложением слоёв, а dp — в пиксели один к одному. Код справа генерируется из того же самого JSON-дерева обходом в глубину, поэтому превью и код физически не могут разойтись. Двигаешь ползунок — меняется одно поле в дереве, и React перерисовывает обе стороны сразу. Кнопка копирования отдаёт именно этот сгенерированный текст — его можно вставить в Android Studio как основу настоящего экрана.

Типичные ошибки

Пять ошибок, с которыми сталкивается почти каждый на первом 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 { ... } } } теряется одна из скобок где-то в середине.

Отвечено вопросов: 0 из 4
Какая из пяти ошибок этой главы связана именно с правилом «composable вызывает только composable»?
Modifier.padding(16) без .dp — какую именно ошибку это вызовет?
Почему у @Preview-функции нельзя оставить параметр без значения по умолчанию (без @PreviewParameter)?
Что общего у ошибок 2, 3 и 5 из этого раздела, в отличие от ошибок 1 и 4?
Отвечено верно: 0 из 4

Слова главы

composable
1 / 8

Потренируйся печатать

Сигнатура composable-функции с аннотацией — то, что придётся набирать перед каждым экраном:

Цель: скорость ≥ 100 зн/мин, точность ≥ 90%

@Composable
fun Greeting(name: String) {

Цепочка модификаторов — самая частая связка символов на первом экране:

Цель: скорость ≥ 120 зн/мин, точность ≥ 90%

Modifier.fillMaxSize().padding(16.dp)
клавиша ``клавиша 11клавиша 22клавиша 33клавиша 44клавиша 55клавиша 66клавиша 77клавиша 88клавиша 99клавиша 00клавиша --клавиша ==клавиша ⌫клавиша TabTabклавиша QQклавиша WWклавиша EEклавиша RRклавиша TTклавиша YYклавиша UUклавиша IIклавиша OOклавиша PPклавиша [[клавиша ]]клавиша \\клавиша Caps LockCaps Lockклавиша AAклавиша SSклавиша DDклавиша FFклавиша GGклавиша HHклавиша JJклавиша KKклавиша LLклавиша ;;клавиша ''клавиша EnterEnterклавиша ShiftShiftклавиша ZZклавиша XXклавиша CCклавиша VVклавиша BBклавиша NNклавиша MMклавиша ,,клавиша ..клавиша //клавиша ShiftShiftклавиша CtrlCtrlклавиша WinWinклавиша AltAltклавиша ПробелПробелклавиша AltAltклавиша WinWinклавиша MenuMenuклавиша CtrlCtrl
следующая: M — указательный правой руки (+ Shift)

Символы и приёмы главы

Символ / приёмКак называетсяЧто делаетПример
@аннотация (собака)помечает объявление метаданными для компилятора или инструментов IDE@Composable
.dpсвойство-расширениепревращает число в единицу измерения Dp, не зависящую от плотности экрана16.dp
Modifier.a().b()цепочка модификаторовкаждый вызов оборачивает результат предыдущего в новый неизменяемый ModifierModifier.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-фазу: каждый модификатор в цепочке — это ещё один слой измерения и расположения, обёрнутый вокруг предыдущего.

Дальше по теме:

Куда дальше: проверенные ресурсы

  • 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 — вход прямо в виджете выше.