Перейти к основному содержимому
Мобилка24ГЛАВА 24Карточка и адаптивность: компонент решает сам1 990 ₽узко1 990 ₽широкоBoxWithConstraintsmaxWidth >= 360компонент решает сам

Карточка и адаптивность: компонент решает сам

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

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

Последний компонент списка — «Создайте компонент карточки». И рядом, отдельной строкой, пункт, который относится ко всей библиотеке сразу: «Все компоненты библиотеки адаптивны».

Второй пункт коварнее первого. «Адаптивность» звучит расплывчато, проверяется придирчиво, а компилятор про неё не скажет ни слова: всё, что её ломает, — совершенно законный код.

Эта глава закрывает оба пункта: карточка как компонент и четыре проверки, из которых складывается адаптивность.

Карточка в списке и в сетке​

ОДНА КАРТОЧКА, ДВЕ РАСКЛАДКИ — РЕШАЕТ ОНА САМА1 990 ₽В корзинуузко: столбиком1 990 ₽В корзинушироко: строкойвызывающий не выбирает вариант: карточка смотрит на доступную ей ширину
Узко — столбиком, широко — строкой; выбирает сама карточка

Одна и та же карточка товара на макете нарисована дважды: в каталоге сеткой по две в ряд (узкая, столбиком) и в корзине списком (широкая, строкой).

Соблазн — сделать два компонента или добавить параметр horizontal: Boolean. Оба решения перекладывают выбор на вызывающего, а он о доступной ширине знает не больше карточки.

Правильно — карточка смотрит на свою ширину:

BoxWithConstraints {
val wide = maxWidth.value >= wideFrom
if (wide) {
Row(…) { image?.invoke(); KitCardTexts(…); action?.invoke() }
} else {
Column(…) { image?.invoke(); KitCardTexts(…); action?.invoke() }
}
}
Нажми на часть выражения

Попытка узнать ширину не в том месте:

@Composable fun P() { Text(maxWidth.toString()) }
e: Probe.kt:4:28 Unresolved reference 'maxWidth'.

Знакомый признак из прошлой главы: имя есть, а контекста нет. maxWidth существует только внутри BoxWithConstraints.

Важно

Вызывающий не должен выбирать вид карточки. Он говорит, сколько места даёт, — и всё. Это тот же принцип, что с modifier: снаружи задают условия, а компонент решает, как в них уложиться.

Четыре проверки адаптивности​

«КОМПОНЕНТ АДАПТИВЕН» — ЭТО ЧЕТЫРЕ ПРОВЕРКИнет жёсткой шириныwidth(320.dp)fillMaxWidth() или widthIn(max = …)текст обрезаетсяText(title)maxLines + TextOverflow.Ellipsisраскладка от ширинывызывающий выбирает видBoxWithConstraints внутривысота от содержимогоheight(180.dp)heightIn(min = …)ни одну из четырёх компилятор не поймает: всё это законный код
Ни одну из четырёх компилятор не поймает

Первая: нет жёсткой ширины.

Card(modifier = Modifier.width(320.dp)) { … }
(сборка проходит, ошибок нет)

Компилятор доволен. На узком экране такая карточка вылезет за край, на широком — оставит пустоту справа. Вместо этого: fillMaxWidth() там, где карточка занимает всю ширину, и widthIn(max = …) там, где надо ограничить рост на планшете.

Вторая: длинный текст обрезается.

Text("Очень длинное название товара, которое не поместится ни на каком экране целиком")
(сборка проходит, ошибок нет)
ТРИ МЕСТА, ГДЕ ЛОМАЕТСЯ ДЛИННОЕ НАЗВАНИЕбез maxLines — карточка растёт вниз, и в сетке соседние встают вразнобойна макете все карточки одной высоты, у вас — лесенкаmaxLines без overflow — текст обрывается на полуслове без многоточиявыглядит как обрезанная вёрстка, а не как сокращениеmaxLines = 2, overflow = TextOverflow.Ellipsisдве строки и многоточие: высота предсказуема, обрыв выглядит намереннымпроверяют это одним товаром с названием на сто знаков — он всегда найдётся в каталоге
Три места, где ломается длинное название

Без maxLines карточка растёт вниз, и в сетке соседние карточки встают лесенкой. С maxLines, но без overflow, текст обрывается на полуслове без многоточия — выглядит как обрезанная вёрстка, а не как сокращение.

Text(
text = title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)

Третья: раскладку выбирает компонент. Разобрана выше.

Четвёртая: высота от содержимого. height(180.dp) фиксирует высоту, и при крупном системном шрифте текст обрежется по нижнему краю. heightIn(min = 180.dp) задаёт минимум, оставляя карточке право вырасти.

Интересный факт

Все четыре проверяются одинаково: переключить размер шрифта в настройках на крупный, повернуть экран и открыть тот товар, у которого самое длинное название. Три действия, полторы минуты — и видно всё, чего не видит компилятор.

Отвечено вопросов: 0 из 3
Кто должен выбирать, столбиком карточка или строкой?
Modifier.width(320.dp) у карточки. Что скажет компилятор?
Почему maxLines без overflow — плохо?
Отвечено верно: 0 из 3

Компонент целиком​

@Composable
fun KitProductCard(
title: String,
price: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
onClick: (() -> Unit)? = null,
image: (@Composable () -> Unit)? = null,
action: (@Composable () -> Unit)? = null,
wideFrom: Int = 360,
) {
Card(
modifier = modifier
.fillMaxWidth()
.then(if (onClick != null) Modifier.clickable { onClick() } else Modifier),
) {
BoxWithConstraints {
val wide = maxWidth.value >= wideFrom
…
}
}
}
Нажми на часть выражения

И тексты отдельной приватной функцией — они одинаковы в обеих раскладках:

@Composable
private fun KitCardTexts(title: String, subtitle: String?, price: String, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(text = title, style = MaterialTheme.typography.titleMedium,
maxLines = 2, overflow = TextOverflow.Ellipsis)
if (subtitle != null) {
Text(text = subtitle, style = MaterialTheme.typography.bodySmall,
maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(text = price, style = MaterialTheme.typography.titleLarge)
}
}
Совет

Заголовок в две строки, подзаголовок в одну, цена без ограничения — это не произвол. Цену обрезать нельзя никогда: «1 990…» хуже, чем сломанная вёрстка, потому что вводит в заблуждение. Всё, что человек может неверно прочитать, обрезать не стоит.

Почему не WindowSizeClass​

Для адаптивности в Android есть готовый механизм — классы размеров окна: компактный, средний, развёрнутый. Он полезен, но решает другую задачу: как перестроить экран целиком — показать список и деталь рядом на планшете вместо двух экранов подряд.

Компоненту библиотеки он не подходит по простой причине: карточка может оказаться узкой и на широком экране — например, в сетке по три в ряд. Класс размера окна скажет «развёрнутый», а места у карточки будет 200 точек.

Важно

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

Отвечено вопросов: 0 из 3
Зачем .then(if (onClick != null) Modifier.clickable { … } else Modifier)?
Почему цену не ограничивают по числу строк?
Почему компонент не смотрит на класс размера окна?
Отвечено верно: 0 из 3

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

Ошибка 1. Жёсткая ширина в компоненте.

width(320.dp) — и карточка вылезает на узком экране.

Ошибка 2. Параметр «горизонтальная ли карточка».

Перекладывает решение на вызывающего, который знает не больше.

Ошибка 3. Текст без ограничения строк.

Сетка превращается в лесенку из-за одного длинного названия.

Ошибка 4. Обрезанная цена.

Разобрано: вводит в заблуждение.

Ошибка 5. Фиксированная высота.

При крупном системном шрифте текст обрежется снизу. heightIn(min = …) вместо height(…).

Ошибка 6. Проверка только на одном устройстве.

Эмулятор с одним разрешением и обычным шрифтом покажет, что всё хорошо. Три действия — крупный шрифт, поворот, самое длинное название — покажут правду.

Ошибка 7. Карточка знает про товар слишком много.

fun KitProductCard(product: Product)

Библиотечный компонент, принимающий тип из предметной области, перестаёт быть библиотечным: его не переиспользовать для категории, акции или пункта заказа. Принимают отдельные значения и слоты.

Отвечено вопросов: 0 из 3
Почему компонент библиотеки не принимает тип Product?
Чем heightIn(min = …) лучше height(…)?
Как проверить адаптивность за полторы минуты?
Отвечено верно: 0 из 3

Шпаргалка по главе​

Что нужноКак
Узнать свою ширинуBoxWithConstraints { maxWidth … }
Ширина карточкиfillMaxWidth() или widthIn(max = …), не width(…)
ВысотаheightIn(min = …), не height(…)
Обрезать заголовокmaxLines = 2, overflow = TextOverflow.Ellipsis
Ценабез ограничения строк
Условный модификатор.then(if (…) Modifier.clickable { … } else Modifier)
Что приниматьотдельные значения и слоты, не тип из предметной области
Проверкакрупный шрифт + поворот + самое длинное название

Слова главы​

ТерминЧто это
BoxWithConstraintsКонтейнер, внутри которого известны собственные ограничения
ОграниченияСколько места родитель разрешил занять: maxWidth, maxHeight
Порог раскладкиШирина, начиная с которой компонент перестраивается
TextOverflow.EllipsisМноготочие вместо обрыва на полуслове
Класс размера окнаКомпактный, средний, развёрнутый — для экрана, не для компонента
Условный модификаторПриклеенный через .then только при выполнении условия

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

BoxWithConstraints {

Что должен уметь после главы​

  • собрать карточку, которая сама выбирает раскладку по доступной ширине;
  • назвать четыре проверки адаптивности и объяснить, почему компилятор молчит на всех;
  • обрезать длинный текст правильно и сказать, что обрезать нельзя;
  • объяснить разницу между ограничениями компонента и классом размера окна;
  • приклеить модификатор условно;
  • проверить адаптивность тремя действиями за полторы минуты.

Проверка по главе​

Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

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

Видео по теме

ВидеоКурс по Jetpack Compose Android | Card, Box, Image | #3 | Android StudioNeco Ru
ВидеоВерстка экрана в Jetpack Compose Beta. Часть 1. [Ru, Android] / Мобильный разработчикАлексей Гладков
ВидеоВерстка экрана в Jetpack Compose Beta. Часть 2. [Ru, Android] / Мобильный разработчикАлексей Гладков
ВидеоAndroid Jetpack Compose Card Implementation | Beginner Tutorialmagic light
ВидеоИспользование ConstrainLayout в Jetpack Compose Android Studio + KotlinNeco Ru

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.