Макет экрана (layout) в Android — это описание структуры интерфейса: какие элементы есть на экране и по каким правилам они расположены друг относительно друга и относительно краев. Сегодня макет описывают двумя способами: XML-разметкой для классической системы View или кодом на Kotlin в Jetpack Compose. Ниже — как выбрать способ, один и тот же экран в обоих вариантах, виды контейнеров и типовые ошибки.
Содержание
Сначала мини-словарь, потому что эти слова часто смешивают:
| Термин | Что это | Пример |
|---|---|---|
| View | Отдельный элемент интерфейса в классической системе | TextView, Button, EditText |
| ViewGroup (контейнер, layout) | View, который размещает дочерние элементы по своим правилам | LinearLayout, ConstraintLayout |
| Composable-функция | Функция Kotlin с аннотацией @Composable, которая описывает часть UI в Compose |
Column, Row, Text |
| Макет в дизайне (мокап) | Картинка или прототип экрана в Figma, по которому потом пишут разметку | Фрейм 360×800 в Figma |
Мокап показывает, как экран должен выглядеть, а layout задает правила, по которым экран подстраивается под разные дисплеи.
XML или Compose: что выбрать
Google называет Jetpack Compose рекомендуемым современным инструментом для нативного UI на Android. При этом XML-разметка никуда не делась: на ней написана большая часть существующих приложений, и в работе с чужим кодом ее придется читать.
| Критерий | XML + View | Jetpack Compose |
|---|---|---|
| Где описан экран | Файл в res/layout/, логика в Activity/Fragment |
Kotlin-функции в том же модуле, что и логика |
| Как обновляется UI | Вручную: найти View и поменять свойство | Декларативно: меняется состояние — экран перерисовывается |
| Списки | RecyclerView + адаптер |
LazyColumn / LazyRow |
| Предпросмотр | Layout Editor в Android Studio | @Preview в Android Studio |
| Когда брать | Поддержка существующего проекта на View | Новый проект или новый экран |
Практическое правило: новый экран в новом проекте — Compose; правка экрана в старом проекте — тот же подход, на котором он написан. Оба подхода можно совмещать в одном приложении (ComposeView внутри XML и AndroidView внутри Compose), так что переход бывает постепенным.
Один экран в Compose
Возьмем простой экран напоминания: поле ввода, строка «дата + время» и кнопка «Готово», прижатая вправо. Чтобы пример оставался коротким, вместо выпадающих списков даты и времени в строке стоят подписи. Минимальный полный пример:
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun ReminderScreen(modifier: Modifier = Modifier) {
var name by rememberSaveable { mutableStateOf("") }
Column(
modifier = modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Напоминание") },
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Дата", modifier = Modifier.weight(1f))
Text("Время", modifier = Modifier.width(96.dp))
}
Button(
onClick = { /* сохранить напоминание */ },
enabled = name.isNotBlank(),
modifier = Modifier.align(Alignment.End)
) {
Text("Готово")
}
}
}
@Preview(showBackground = true)
@Composable
fun ReminderScreenPreview() {
ReminderScreen()
}
В панели Preview появится экран: поле во всю ширину, под ним строка, где «Дата» занимает все свободное место, а «Время» — ровно 96 dp, и неактивная кнопка «Готово» справа. Как только в поле появится текст, кнопка станет активной — это и есть декларативность: условие enabled пересчитывается при изменении состояния name.
Разбор по частям:
ColumnиRow— аналоги вертикального и горизонтальногоLinearLayout,Box— аналогFrameLayout(элементы друг поверх друга).Modifierзадает размер, отступы и выравнивание конкретного элемента. Порядок модификаторов важен:paddingдоfillMaxSizeи после дают разный результат.weight(1f)отдает элементу оставшееся место в строке — какlayout_weightв XML.rememberSaveableхранит введенный текст и переживает поворот экрана; обычныйrememberпри пересоздании Activity значение потеряет.
Для сложной относительной раскладки в Compose тоже есть ConstraintLayout, но это отдельная библиотека (constraintlayout-compose), и для большинства экранов хватает вложенных Column/Row/Box: вложенность в Compose не несет того штрафа производительности, что глубокие деревья View.
Тот же экран в XML на ConstraintLayout
В системе View экран описывают в файле res/layout/activity_reminder.xml, а в Activity подключают через setContentView(R.layout.activity_reminder). Здесь в строке даты и времени — выпадающие списки Spinner, как в исходной форме:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<EditText
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/reminder"
android:inputType="text"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Spinner
android:id="@+id/dates"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/times" />
<Spinner
android:id="@+id/times"
android:layout_width="96dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/done"
app:layout_constraintTop_toBottomOf="@id/times"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Каждый элемент привязан ограничениями (constraints) к родителю (parent) или к соседу по id. Правило для ConstraintLayout: у каждого элемента должна быть хотя бы одна привязка по горизонтали и одна по вертикали, иначе его положение на реальном устройстве не определено.
Ключевые детали:
@+id/timesуdatesстоит с плюсом, потому что элементtimesобъявлен ниже: плюс создает id при первом упоминании.0dpу ширины в ConstraintLayout означает «match constraints»: элемент растягивается между своими привязками. Полеnameзанимает все пространство от левого до правого края,dates— от левого края доtimes.match_parentдля дочерних элементов ConstraintLayout документация не рекомендует — вместо него используют0dpс привязками к обоим краям.Start/EndвместоLeft/Right— чтобы раскладка корректно зеркалилась в языках с письмом справа налево.
Виды контейнеров в XML
Старые руководства смешивают контейнеры, списки и устаревшие классы. Актуальная картина:
| Контейнер | Как размещает элементы | Статус и когда брать |
|---|---|---|
LinearLayout |
В одну строку или столбец, layout_weight делит место |
Простые формы и списки полей |
FrameLayout |
Слоями друг поверх друга | Один дочерний элемент, наложение, контейнер для фрагмента |
ConstraintLayout |
Ограничениями к родителю и соседям, цепочки, направляющие | Основной выбор для сложного экрана в XML |
RelativeLayout |
Относительно родителя и соседей (layout_below, layout_toStartOf) |
Встречается в старом коде, для нового — ConstraintLayout |
TableLayout, GridLayout |
Строки и столбцы | Редко, обычно заменяет ConstraintLayout или список |
AbsoluteLayout |
По координатам x, y | Устарел давно, не использовать: ломается на других экранах |
Списки — отдельная история. ListView и GridView из старых статей — это не макеты, а виджеты для прокручиваемых наборов данных; в современном View-коде вместо них берут RecyclerView с LinearLayoutManager или GridLayoutManager. Вкладки делают через TabLayout из Material Components, а не через устаревший TabHost.
Как атрибуты RelativeLayout переносятся в ConstraintLayout
Если вы поддерживаете старый экран, пригодится таблица соответствий:
| RelativeLayout | ConstraintLayout |
|---|---|
layout_below="@id/a" |
layout_constraintTop_toBottomOf="@id/a" |
layout_above="@id/a" |
layout_constraintBottom_toTopOf="@id/a" |
layout_toStartOf="@id/a" |
layout_constraintEnd_toStartOf="@id/a" |
layout_toEndOf="@id/a" |
layout_constraintStart_toEndOf="@id/a" |
layout_alignTop="@id/a" |
layout_constraintTop_toTopOf="@id/a" |
layout_alignBaseline="@id/a" |
layout_constraintBaseline_toBaselineOf="@id/a" |
layout_alignParentEnd="true" |
layout_constraintEnd_toEndOf="parent" |
layout_centerInParent="true" |
Привязки ко всем четырем сторонам parent |
В RelativeLayout атрибуты пишутся с префиксом android:, в ConstraintLayout — с app:, потому что это библиотека AndroidX, а не часть платформы.
Цепочки и направляющие
Цепочка (chain) — группа элементов, связанных ограничениями друг с другом в обе стороны. Стиль задают у первого элемента атрибутом layout_constraintHorizontal_chainStyle: spread делит свободное место поровну между элементами и краями, spread_inside прижимает крайние элементы к краям, packed собирает элементы вместе по центру.
Направляющая (Guideline) — невидимая линия, к которой привязывают элементы. Ее позицию задают в dp от края (layout_constraintGuide_begin) или долей ширины (layout_constraintGuide_percent="0.3"). Удобно, когда у нескольких элементов общий отступ: меняете одну направляющую, а не каждый элемент.
Если не получилось
| Симптом | Частая причина | Что сделать |
|---|---|---|
| В редакторе элемент на месте, на устройстве — в левом верхнем углу | Нет привязок в ConstraintLayout, позиция задана только для редактора (tools:layout_editor_*) |
Добавить ограничения по горизонтали и вертикали; Android Studio подсвечивает такой элемент предупреждением lint |
Элемент с 0dp не виден |
Ширина 0dp, но привязан только к одной стороне |
Привязать к обеим сторонам или поставить wrap_content |
| Контент залезает под статус-бар и панель навигации | На новых версиях Android приложение по умолчанию рисуется от края до края (edge-to-edge) | В Compose — Scaffold и его innerPadding или Modifier.safeDrawingPadding(); в View — обработка window insets |
| В Compose после поворота экрана пропал текст | Состояние в remember вместо rememberSaveable |
Хранить ввод в rememberSaveable или во ViewModel |
Выводы
- Макет экрана Android — это правила расположения элементов, а не картинка: мокап в Figma задает вид, layout в коде — поведение на разных экранах.
- Для нового проекта Google рекомендует Jetpack Compose (
Column,Row,Box,Modifier), XML с View остается стандартом в существующем коде, и эти подходы совмещаются. - В XML основной контейнер для сложных экранов — ConstraintLayout: каждому элементу нужна привязка по обеим осям,
0dpозначает растяжение между привязками. - RelativeLayout, ListView, TabHost и AbsoluteLayout из старых руководств — наследие; их аналоги — ConstraintLayout, RecyclerView, TabLayout и адаптивная раскладка.
Где применяется / связь с практикой
Освойте тему на практике
Верстка экранов — первое, с чем сталкивается начинающий Android-разработчик: любой учебный проект, от списка дел до клиента погоды, начинается с макета главного экрана, затем добавляются списки, навигация и работа с состоянием. Чтобы собрать такое приложение целиком под руководством практикующих разработчиков, посмотрите курс Android Developer. Basic. Разобрать отдельные темы бесплатно можно на открытых уроках Otus.
Смежная тема: что собой представляет Android Studio.
FAQ
Что значат единицы dp и sp в макете?
dp (density-independent pixels) — единица размеров, которая пересчитывается под плотность экрана, поэтому кнопка 48 dp выглядит примерно одинаково на разных телефонах. sp используют для текста: он дополнительно учитывает размер шрифта, заданный пользователем в настройках.
Можно ли создать интерфейс без XML и без Compose, прямо кодом?
Да, View можно создавать в Kotlin или Java (LinearLayout(context), addView(...)), но такой код хуже читается и не имеет предпросмотра. Обычно так добавляют отдельные элементы динамически, а основу экрана оставляют в XML или Compose.



