Перейти к основному содержимому
Мобилка23ГЛАВА 23Хэдер и TabBar: шапка и нижняя панель как компоненты←Каталог⋮содержимоеКатКорзЯ3одинаково на всех экранах

Хэдер и TabBar: шапка и нижняя панель как компоненты

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

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

В критериях два отдельных пункта: «Создайте компонент TabBar» и «Создайте компонент „Хэдер“». И тут возникает законный вопрос: разве мы это уже не проходили? В главе про Scaffold были и шапка, и нижняя панель.

Разница ровно та же, что была с кнопкой: Scaffold — каркас экрана, готовый и общий, а хэдер и панель — ваши компоненты, которые в этот каркас вставляют. Каркас берут как есть; компоненты пишут так, чтобы на всех экранах приложения они выглядели и вели себя одинаково. Это и оценивается.

Заодно здесь встретится ошибка, которая уже была в главе про кнопки, — и это не совпадение.

Каркас и компоненты​

КАРКАС ЭКРАНА И КОМПОНЕНТ БИБЛИОТЕКИ — РАЗНЫЕ ВЕЩИScaffoldtopBar = { KitHeader(…) }содержимое экранаbottomBar = { KitTabBar(…) }Scaffold — каркасраскладывает четыре слота и считаетотступы, чтобы содержимое не уехалопод шапку и под панельKitHeader и KitTabBar — компонентываши, переиспользуемые, с одинаковым видомна всех экранах — это и есть пункт критериевкаркас берут готовый, компоненты пишут свои и вставляют в его слоты
Scaffold раскладывает слоты; что в них лежит — ваше дело
Scaffold(
topBar = { KitHeader(title = "Каталог", actions = { KitButtonGhost("Фильтр", onClick = {}) }) },
bottomBar = { KitTabBar(tabs = tabs, selectedId = current, onSelect = ::go) },
) { innerPadding ->
CatalogList(modifier = Modifier.padding(innerPadding))
}

Работа Scaffold — сложить четыре слота и посчитать отступы, чтобы содержимое не уехало под шапку и под панель. Работа KitHeader и KitTabBar — выглядеть одинаково на всех двенадцати экранах.

Совет

Признак, что компонент нужен: вы второй раз пишете TopAppBar(title = { Text(...) }, navigationIcon = { ... }) с одними и теми же настройками. На третьем экране отличие уже проберётся — где-то другой отступ, где-то другой цвет. Компонент библиотеки существует именно для того, чтобы отличий не было.

Шапка: три места​

ШАПКА — ЭТО ТРИ МЕСТА, А НЕ ОДНА СТРОКАnavigationКаталогactionsnavigationназад, закрыть, меню — то, что уводит с экранаtitleкуда человек попал; на главной может отсутствоватьactionsдействия над содержимым: поиск, фильтр, корзинапараметр centered меняет только положение заголовка — это два разных вида шапки в Materialслоты необязательные: на главной нет «назад», на экране без действий пуст правый край
Слева уводящее действие, посередине заголовок, справа действия над содержимым
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KitHeader(
title: String,
modifier: Modifier = Modifier,
centered: Boolean = false,
navigation: (@Composable () -> Unit)? = null,
actions: (@Composable () -> Unit)? = null,
) {
val titleSlot: @Composable () -> Unit = { Text(text = title) }
val navSlot: @Composable () -> Unit = { navigation?.invoke() }
val actionsSlot: @Composable () -> Unit = { actions?.invoke() }

if (centered) {
CenterAlignedTopAppBar(
title = titleSlot,
modifier = modifier,
navigationIcon = navSlot,
actions = { Row { actionsSlot() } },
)
} else {
TopAppBar(
title = titleSlot,
modifier = modifier,
navigationIcon = navSlot,
actions = { Row { actionsSlot() } },
)
}
}
Нажми на часть выражения
Важно

Обратите внимание на несимметричность: заголовок — строка, а navigation и actions — слоты. Это не непоследовательность. Правило простое: слот там, где содержимое заранее неизвестно. Заголовок всегда текст, а слева может быть стрелка, крестик или три полоски.

Отвечено вопросов: 0 из 3
Чем KitHeader отличается от Scaffold?
Почему заголовок — строка, а navigation и actions — слоты?
Зачем в actions обёртка Row?
Отвечено верно: 0 из 3

Нижняя панель​

data class KitTab(
val id: String,
val label: String,
val badge: Int? = null,
)

@Composable
fun KitTabBar(
tabs: List<KitTab>,
selectedId: String,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier,
icon: @Composable (KitTab) -> Unit = {},
) {
NavigationBar(modifier = modifier) {
tabs.forEach { tab ->
NavigationBarItem(
selected = tab.id == selectedId,
onClick = { onSelect(tab.id) },
icon = {
if (tab.badge != null && tab.badge > 0) {
BadgedBox(badge = { Badge { Text(tab.badge.toString()) } }) { icon(tab) }
} else {
icon(tab)
}
},
label = { Text(tab.label) },
alwaysShowLabel = true,
)
}
}
}
Нажми на часть выражения

Кто помнит выбранную вкладку​

ПАНЕЛЬ НЕ ПОМНИТ, ЧТО ВЫБРАНОПомнит самаvar selected by remember { … }вернулись на экран — вкладка сбросиласьперешли по ссылке извне — панель не знаетнавигация и панель разошлисьПолучает снаружиselectedId: StringonSelect: (String) -> Unitисточник истины один — текущий маршрутпанель всегда согласована с экраномто же правило, что у поля ввода: компонент показывает и сообщает, а помнит вызывающийвыбранную вкладку вычисляют из текущего маршрута навигации, а не хранят отдельнодва источника истины всегда расходятся — вопрос только в том, когда это заметят
Два источника истины всегда расходятся — вопрос только в том, когда это заметят

Соблазн — хранить выбранную вкладку внутри панели: var selected by remember { … }. Так короче, и на первый взгляд работает.

Сломается это в трёх местах:

  • вернулись на экран — панель сбросилась на первую вкладку, а показывается третья;
  • перешли из уведомления или по ссылке — открылся экран корзины, а подсвечен каталог;
  • нажали «назад» — экран сменился, панель нет.

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

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

val current = navController.currentBackStackEntryAsState().value?.destination?.route ?: "catalog"

KitTabBar(
tabs = tabs,
selectedId = current,
onSelect = { navController.navigate(it) },
)
Важно

Источник истины один — текущий маршрут навигации. Панель не хранит ничего, и рассогласоваться нечему. Это тот же принцип, что и у состояния из главы про ViewModel: если одно и то же значение живёт в двух местах, они разойдутся — вопрос только в том, когда это заметят.

Отвечено вопросов: 0 из 3
Почему выбранную вкладку не хранят внутри панели?
Как вычисляют выбранную вкладку?
Зачем проверка tab.badge > 0?
Отвечено верно: 0 из 3

Ошибка, которая уже была​

@Composable fun P() { NavigationBarItem(selected = true, onClick = {}, icon = { Text("к") }) }
e: Probe.kt:5:23 Unresolved reference 'NavigationBarItem'.

Имя написано правильно, импорт на месте — а ссылка не находится. Это ровно та же история, что с Modifier.weight в главе про кнопки: NavigationBarItem объявлена как расширение для RowScope, а он существует только внутри NavigationBar { … }.

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

«Неразрешённая ссылка» на функцию, которая точно существует, почти всегда означает одно: вы вне того контекста, для которого она объявлена. weight — внутри Row или Column, align с двумя координатами — внутри Box, NavigationBarItem — внутри NavigationBar. Полезнее запомнить признак, чем список.

Вторая ошибка проще:

NavigationBar { NavigationBarItem(onClick = {}, icon = { Text("к") }) }
e: Probe.kt:6:71 No value passed for parameter 'selected'.

У NavigationBarItem нет умолчания для selected — и это осознанно: вкладка обязана знать, выбрана она или нет, а разумного значения по умолчанию тут не существует.

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

Ошибка 1. Хранить выбранную вкладку в панели.

Разобрано выше: рассогласование с навигацией.

Ошибка 2. Значок с нулём.

Пустая корзина носит «0». Проверка на > 0 — одна строка.

Ошибка 3. Подписи прячутся.

alwaysShowLabel по умолчанию скрывает подписи невыбранных вкладок. Если на макете видны все — параметр надо задать явно.

Ошибка 4. Шапка собирается заново на каждом экране.

Двенадцать экранов — двенадцать слегка разных шапок. Это ровно то, против чего пункт критериев.

Ошибка 5. Scaffold есть, а innerPadding не использован.

Содержимое уезжает под шапку. Ошибка не из этой главы, но проявляется именно здесь — когда шапка наконец появилась.

Ошибка 6. Больше пяти вкладок.

Material рассчитан на три–пять. Шесть не помещаются по ширине на узком экране, подписи начинают обрезаться.

Отвечено вопросов: 0 из 3
NavigationBarItem даёт «Unresolved reference», хотя импорт на месте. Почему?
Почему у selected нет значения по умолчанию?
Сколько вкладок помещается в нижнюю панель?
Отвечено верно: 0 из 3

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

Что нужноКак
Шапка в каркасеScaffold(topBar = { KitHeader(…) })
Панель в каркасеScaffold(bottomBar = { KitTabBar(…) })
Разрешить шапку@OptIn(ExperimentalMaterial3Api::class)
Заголовок по центруотдельный компонент CenterAlignedTopAppBar
Вызов необязательного слотаnavigation?.invoke()
Несколько действий справаactions = { Row { … } }
Выбранная вкладкаselected = tab.id == selectedId, а selectedId — из маршрута
Значок со счётчикомBadgedBox + проверка badge > 0
Подписи всех вкладокalwaysShowLabel = true

Слова главы​

ТерминЧто это
Каркас экранаScaffold: раскладывает слоты и считает отступы
ШапкаВерхняя полоса: уводящее действие, заголовок, действия
Нижняя панельПереключение основных разделов приложения
Значок-счётчикBadge: число поверх значка вкладки
Источник истиныЕдинственное место, где значение хранится
Расширение для областиФункция, доступная только внутри своего контейнера

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

TopAppBar(title = titleSlot, navigationIcon = navSlot)

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

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

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

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

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

Видео по теме

ВидеоScaffold и TopAppBar в Jetpack Compose | Android Studio - Kotlin | #21Neco Ru
ВидеоBottomNavigation в Jetpack Compose | Android Studio - Kotlin | #20Neco Ru
ВидеоScaffold (TopAppBar, BottomBar & FAB) - Android Jetpack Compose 12Neat Roots
ВидеоAndroid Compose Scaffold | Designing TopAppBar Basics: Jetpack Compose - 23Codetutor
ВидеоNavigation в Jetpack Compose | Android Studio - Kotlin | #24Neco Ru

Комментарии

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