Чистый код: комментарии и логирование, которые оценивают
- Зачем это нужно
- Это множитель, а не критерий
- Арифметика: почему аккуратный середняк обгоняет идеалиста
- Почему код в теле
@Composableвыполняется десятки раз - Куда переезжает вынесенное
- Карточка списка рисует, а не считает
- Одна функция — одна причина меняться
- Имена, после которых комментарий не нужен
- Три антипаттерна, которые видно с первого взгляда
- Шапка класса по формату задания
- Что комментировать, а о чём молчать
- Уровни логирования и формат строки
- Обязательные точки: где лог не опция
- Разбор инцидента: два журнала об одном сбое
- Чек-лист перед сдачей сессии
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Эта глава — про то, что легче всего пропустить и дороже всего пропускать.
Комментарии и логирование выглядят как аккуратность: сделал работу — оформи. На чемпионате они устроены иначе. В ведомости оценки против большинства аспектов стоит оговорка: половина итоговой оценки, если отсутствуют комментарии и логирование согласно заданию. Это не отдельный критерий, который можно не добрать. Это множитель на почти всё остальное.
Вторая половина главы — про то, где вообще должен жить код, чтобы его было за что хвалить: почему логика не пишется в разметке, зачем разрезать длинную функцию и чем God ViewModel отличается от рабочей.
Читать её стоит не в конце, а до того, как написан первый экран. Всё, что здесь описано, делается по ходу работы за минуты и почти неисполнимо за полчаса до сдачи.
Это множитель, а не критерий
Разница между «отдельным критерием» и «множителем» решает всё.
Отдельный критерий можно не добрать: не сделал — потерял его баллы, остальное осталось. Множитель работает иначе: он режет то, что вы уже заработали.
Отсюда следствие, которое стоит принять до того, как начнётся сессия: комментарии и логирование — не то, что делают, если осталось время. Это то, без чего остальная работа стоит вдвое меньше.
Арифметика: почему аккуратный середняк обгоняет идеалиста
Посчитаем на модели. Заодно она проверяет пять типовых шапок класса на соответствие требованию — и показывает, какие из них не проходят.
// Модель приёмки: эксперт смотрит шапку класса и ищет три вещи —
// описание, дату создания ДД-ММ-ГГГГ и номер участника. Проверим пять реальных шапок.
val dateRe = Regex("""\b\d{2}-\d{2}-\d{4}\b""")
val numberRe = Regex("""(?iu)участник\w*\s*[:№]?\s*\d+""")
fun check(header: String): List<String> {
val body = header.lines().map { it.trim().removePrefix("*").removePrefix("//").trim() }
.filter { it.isNotEmpty() && it != "/**" && it != "*/" }
val missing = mutableListOf<String>()
val described = body.any { it.length >= 15 && !dateRe.containsMatchIn(it) && !numberRe.containsMatchIn(it) }
if (!described) missing += "описание (от 15 символов)"
if (!dateRe.containsMatchIn(header)) missing += "дата ДД-ММ-ГГГГ"
if (!numberRe.containsMatchIn(header)) missing += "номер участника"
return missing
}
fun main() {
val headers = linkedMapOf(
"пусто" to "",
"// экран корзины" to "// экран корзины",
"KDoc без даты" to """
/**
* Репозиторий корзины: загрузка позиций с сервера и кэш в Room.
* Участник: 07
*/""".trimIndent(),
"дата в ISO" to """
/**
* Репозиторий корзины: загрузка позиций с сервера и кэш в Room.
* Дата создания: 2026-09-05
* Участник: 07
*/""".trimIndent(),
"по регламенту" to """
/**
* Репозиторий корзины: загрузка позиций с сервера и кэш в Room.
* Дата создания: 05-09-2026
* Участник: 07
*/""".trimIndent(),
)
headers.forEach { (name, h) ->
val miss = check(h)
val verdict = if (miss.isEmpty()) "принято" else "не хватает: ${miss.joinToString(", ")}"
println(" %-16s → %s".format(name, verdict))
}
println()
println("── во что обходится пропуск ──")
val total = 100.0
val underMultiplier = 86.0 // аспектов с оговоркой «половина оценки без комментариев и логирования»
val free = total - underMultiplier
fun score(raw: Double, hasComments: Boolean): Double {
val part = raw / total
val m = if (hasComments) 1.0 else 0.5
return free * part + underMultiplier * part * m
}
val a = score(100.0, hasComments = false)
val b = score(65.0, hasComments = true)
println(" участник А: приложение сделано на 100%%, комментариев и логов нет → %.1f".format(a))
println(" участник Б: приложение сделано на 65%%, комментарии и логи есть → %.1f".format(b))
println(" разрыв: %.1f балла в пользу Б".format(b - a))
println()
val classes = 20
val secondsPerHeader = 30
val minutes = classes * secondsPerHeader / 60
println(" цена вопроса: $classes шапок по $secondsPerHeader сек = $minutes минут набора")
println(" что они спасают: %.1f балла".format(underMultiplier / 2))
}
Живой вывод:
пусто → не хватает: описание (от 15 символов), дата ДД-ММ-ГГГГ, номер участника
// экран корзины → не хватает: описание (от 15 символов), дата ДД-ММ-ГГГГ, номер участника
KDoc без даты → не хватает: дата ДД-ММ-ГГГГ
дата в ISO → не хватает: дата ДД-ММ-ГГГГ
по регламенту → принято
── во что обходится пропуск ──
участник А: приложение сделано на 100%, комментариев и логов нет → 57.0
участник Б: приложение сделано на 65%, комментарии и логи есть → 65.0
разрыв: 8.0 балла в пользу Б
цена вопроса: 20 шапок по 30 сек = 10 минут набора
что они спасают: 43.0 балла
Участник, сделавший приложение полностью, проигрывает участнику, сделавшему две трети. Не потому, что тот лучше программист, а потому, что первый не потратил десять минут.
И обратите внимание на две строки в середине: «KDoc без даты» и «дата в ISO» проваливаются одинаково. Запись 2026-09-05 выглядит правильной и профессиональной — но требование говорит ДД-ММ-ГГГГ, то есть 05-09-2026. Формат проверяется буквально.
Почему код в теле @Composable выполняется десятки раз
Теперь о том, где код должен жить. Начнём с самого частого промаха — логики внутри разметки.
Тело @Composable — не «нарисовать один раз». Это функция, которую движок вызывает заново при каждом изменении состояния: на каждый введённый символ, на каждый кадр анимации, на каждое обновление списка при прокрутке. Смоделируем одну работу пользователя — шесть нажатий в поиске плюс прокрутка:
// Модель рекомпозиции: «движок» — обычная функция, которая вызывает тело экрана
// заново при каждом изменении состояния. Считаем, сколько раз выполнится код в теле.
var formattersCreated = 0
var filterPasses = 0
class PriceFormatter(private val suffix: String) {
init { formattersCreated++ }
fun format(v: Int): String = "$v $suffix"
}
data class Product(val id: Int, val title: String, val price: Int)
val catalog = List(30) { Product(it, "товар $it", 100 + it * 7) }
// ── вариант А: логика и объекты живут в теле композабла ──
fun productScreenBad(query: String) {
val visible = catalog.filter { it.title.contains(query) } // фильтр в теле
filterPasses++
for (p in visible) { // тело ProductCard
val fmt = PriceFormatter("руб") // объект на каждый элемент
fmt.format(p.price)
}
}
// ── вариант Б: объект создан один раз, список посчитан во ViewModel ──
fun productScreenGood(visible: List<Product>, fmt: PriceFormatter) {
for (p in visible) fmt.format(p.price) // тело только рисует
}
fun main() {
val keystrokes = listOf("т", "то", "тов", "това", "товар", "товар ")
val animationFrames = 24 // прокрутка списка
val recompositions = keystrokes.size + animationFrames
formattersCreated = 0; filterPasses = 0
keystrokes.forEach { productScreenBad(it) }
repeat(animationFrames) { productScreenBad("товар ") }
println("── А: логика в теле @Composable ──")
println(" рекомпозиций: $recompositions")
println(" проходов фильтра по каталогу: $filterPasses")
println(" создано объектов PriceFormatter: $formattersCreated")
formattersCreated = 0; filterPasses = 0
val visible = catalog.filter { it.title.contains("товар ") } // это посчитал ViewModel
filterPasses++
val fmt = PriceFormatter("руб") // это создал remember
keystrokes.forEach { productScreenGood(visible, fmt) }
repeat(animationFrames) { productScreenGood(visible, fmt) }
println()
println("── Б: логика вынесена, тело только рисует ──")
println(" рекомпозиций: $recompositions")
println(" проходов фильтра по каталогу: $filterPasses")
println(" создано объектов PriceFormatter: $formattersCreated")
println()
println("── ловушка: var объявлен внутри тела ──")
class ScreenState { var clicks = 0 } // живёт вне тела: remember / ViewModel
val hoisted = ScreenState()
var shownByLocalVar = 0
repeat(5) {
var clicks = 0 // новая переменная на каждый вызов тела
hoisted.clicks++
clicks++
shownByLocalVar = clicks
}
println(" после 5 нажатий локальная переменная показывает: $shownByLocalVar")
println(" вынесенное состояние показывает: ${hoisted.clicks}")
}
Живой вывод:
── А: логика в теле @Composable ──
рекомпозиций: 30
проходов фильтра по каталогу: 30
создано объектов PriceFormatter: 900
── Б: логика вынесена, тело только рисует ──
рекомпозиций: 30
проходов фильтра по каталогу: 1
создано объектов PriceFormatter: 1
── ловушка: var объявлен внутри тела ──
после 5 нажатий локальная переменная показывает: 1
вынесенное состояние показывает: 5
Девятьсот созданных объектов против одного — при абсолютно одинаковом результате на экране. Вот почему «логика не живёт в разметке» — не вопрос вкуса и не совет по стилю.
Последние две строки вывода — отдельная ловушка, на которой спотыкаются все. Переменная, объявленная внутри тела, пересоздаётся при каждом вызове. Отсюда классическая жалоба: «нажимаю пять раз, а счётчик показывает единицу». Состояние обязано жить снаружи — в remember или во ViewModel, о чём была отдельная глава.
Куда переезжает вынесенное
Три адреса вместо тела разметки, и выбор между ними не произвольный:
remember— объект, который нужен экрану и живёт вместе с ним: форматтер, состояние прокрутки,SnackbarHostState.ViewModel— состояние экрана и всё, что его считает: фильтрация, сортировка, подготовка данных. Переживает поворот.SideEffectи другие эффекты — действия, которые обязаны случиться при изменении состояния, но не являются отрисовкой: отправка события в аналитику, синхронизация с внешним объектом.
Правило простое: если строка кода не влияет на то, что появится на экране, ей не место в теле разметки.
Отдельно про логирование: Log.d(…) прямо в теле композабла — самая коварная из этих ошибок. Строка появляется в Logcat, всё выглядит рабочим, а журнал распухает в десятки раз: на тридцать рекомпозиций приходится тридцать одинаковых записей. Потом в этом журнале нужно найти настоящую ошибку. Логируют события, а не отрисовку: нажатие, запрос, результат.
Карточка списка рисует, а не считает
Частный, но самый частый случай — элемент списка. В главе про списки мы уже говорили, что тело items { } выполняется для каждого видимого элемента при каждой прокрутке. Значит форматирование цены, разбор даты и склонение слов внутри карточки умножаются на количество элементов и на число кадров.
// плохо: карточка считает
@Composable
fun ProductCard(product: Product) {
val price = NumberFormat.getCurrencyInstance(Locale("ru")).format(product.priceRub)
val date = SimpleDateFormat("dd MMMM", Locale("ru")).format(Date(product.addedAt))
Text("$price · добавлен $date")
}
// хорошо: карточка получает готовое
@Composable
fun ProductCard(item: ProductUi) {
Text("${item.price} · добавлен ${item.addedDate}")
}
Второй вариант — та самая UI-модель из главы про слои. Она существует ровно для этого: карточка получает строки, готовые к показу, и не знает, как они получились.
Одна функция — одна причина меняться
Обработчик нажатия на восемьдесят строк — не редкость на чемпионате: там валидация, запрос, разбор ответа, обновление состояния и показ сообщения. Работает. Но меняться такая функция будет по четырём разным поводам: поменялись правила валидации, поменялся адрес запроса, поменялся формат ответа, поменялся текст сообщения.
Разрезать её стоит по этим самым поводам:
fun onSubmitClick() {
val error = validate(email, password) // меняется, когда меняются правила
if (error != null) { showError(error); return }
viewModelScope.launch { submit(email, password) } // меняется, когда меняется сценарий
}
Признак, по которому режут, — не длина, а число причин для изменения. Функция на тридцать строк, которая делает одно дело, лучше трёх функций по десять, разрезанных механически.
Имена, после которых комментарий не нужен
Хорошее имя снимает необходимость в комментарии. Правило: имя отвечает на вопрос «что это», а не «какого это типа».
// было
val list: List<Product> = … // список чего?
val flag = true // флаг чего?
val data = repository.get() // какие данные?
fun handle(x: Int) { … } // обработать что и как?
// стало
val visibleProducts: List<Product> = …
val isEmailValid = true
val userProfile = repository.getProfile()
fun onQuantityChanged(newQuantity: Int) { … }
Отдельная привычка, которая экономит время на защите: имя обработчика начинается с on — onRetryClick, onQueryChange. Тогда по имени видно, что это событие снизу вверх, а не действие, которое компонент выполняет сам. Это прямо связано с однонаправленным потоком из главы про ViewModel.
Три антипаттерна, которые видно с первого взгляда
God ViewModel — одна ViewModel на всё приложение. Растёт до тысячи строк, в ней состояние всех экранов сразу, и любое изменение задевает всё. У каждого экрана своя.
Utils.kt на шестьсот строк — файл, куда складывают всё, чему не нашлось места. Признак: в имени файла нет темы. Вместо него — файлы по темам: DateFormatting.kt, Validation.kt, или, ещё лучше, функции-расширения рядом с тем типом, который они расширяют.
Вложенность в пять уровней — if внутри for внутри if внутри let внутри launch. Лечится ранним выходом: проверили условие — вышли, а не углубились.
// было: три уровня и обратная логика
fun process(user: User?) {
if (user != null) {
if (user.isActive) {
if (user.hasSubscription) { grantAccess(user) }
}
}
}
// стало: сначала отсекаем, потом делаем
fun process(user: User?) {
if (user == null) return
if (!user.isActive) return
if (!user.hasSubscription) return
grantAccess(user)
}
Шапка класса по формату задания
Требование конкурсного задания дословно: для каждого создаваемого класса верхнего уровня в коде должны присутствовать структурированные комментарии, включающие описание назначения класса, дату создания в формате ДД-ММ-ГГГГ и номер участника.
/**
* Экран корзины: показывает выбранные товары, считает итог
* и отправляет заказ через CartRepository.
*
* Дата создания: 05-09-2026
* Автор создания: Участник 7
*/
@Composable
fun CartScreen(...) { ... }
Публичные методы описывают отдельно: назначение, входные параметры и что возвращается — кроме тривиальных геттеров. Внутри методов комментируют то, что длиннее пяти строк и не очевидно из кода.
Заведите шапку сразу при создании файла, а не потом. Дата в ней — дата создания, и восстанавливать её по памяти в конце сессии придётся для двадцати файлов сразу. Плюс правки в двадцати файлах за полчаса до сдачи — верный способ сломать сборку, а «не компилируется — не оценивается».
Что комментировать, а о чём молчать
Комментарий объясняет почему, а не пересказывает что.
// бесполезно: пересказ кода
counter++ // увеличиваем счётчик на единицу
// полезно: объясняет решение
// Сервер возвращает цену в копейках, чтобы избежать дробей.
// Делим здесь, на границе слоёв, чтобы дальше по коду цена была в рублях.
val priceRub = dto.priceKopecks / 100.0
Первый вид комментариев эксперт видит сразу и читает как отписку: формально комментарии есть, описания назначения нет. Второй объясняет то, чего в коде не видно, — причину.
Хорошая проверка: если комментарий можно удалить, переименовав переменную, — переименуйте вместо того, чтобы комментировать.
Уровни логирования и формат строки
Три уровня, и путать их не стоит:
| Уровень | Когда | Пример |
|---|---|---|
Log.d — DEBUG | подробности для отладки: что пришло, что посчиталось | ответ сервера, содержимое кэша |
Log.i — INFO | заметные события: экран открылся, кнопка нажата | жизненный цикл, действия пользователя |
Log.e — ERROR | сбой: исключение, отказ сервера | каждый catch |
Формат строки задан заданием: [Тег_компонента]: Событие — Детали.
private const val TAG = "CartViewModel"
Log.i(TAG, "[CartViewModel]: Загрузка корзины — начата, userId=$userId")
Log.d(TAG, "[CartViewModel]: Ответ получен — товаров: ${items.size}")
Log.e(TAG, "[CartViewModel]: Ошибка загрузки — ${e.message}", e)
Три детали, о которых узнают поздно.
println — не логирование. Строка появится, и именно поэтому ошибку не замечают: вывод уходит в системный поток, а не в Logcat, и его не отфильтровать по тегу и уровню.
Тег не длиннее 23 символов. Проверка LongLogTag выдаёт «The logging tag can be at most 23 characters» и роняет сборку. ProductDetailsScreenViewModel — уже двадцать девять.
Последним параметром Log.e передают исключение — тогда в журнал попадёт стек, а не только текст сообщения.
Обязательные точки: где лог не опция
Задание перечисляет их поимённо, и проверяют именно их:
- Жизненный цикл экранов —
onCreate,onStart,onResume,onPause,onStop,onDestroy— уровеньINFO. - Сетевые запросы и работа с базой — начало
INFO, успехDEBUG, ошибкаERROR. - Нажатия критических кнопок —
INFO. - Каждый
try-catch—ERROR. Без исключений: пустойcatchилиe.printStackTrace()требование не закрывают.
Последний пункт стоит перечитать. Пустой catch — это не только потерянный балл: экран остаётся пустым, приложение не падает, в журнале ничего. Отладить такое невозможно в принципе.
Разбор инцидента: два журнала об одном сбое
Зачем нужен формат, видно, только когда что-то сломалось. Смоделируем один и тот же сбой в двух журналах и попробуем найти причину в каждом:
// Модель разбора инцидента. Logcat — это текст, который потом фильтруют.
// Собираем два журнала об ОДНОМ И ТОМ ЖЕ сбое и пробуем найти причину в каждом.
enum class Level { DEBUG, INFO, ERROR }
data class Line(val ms: Int, val level: Level, val text: String)
class Journal {
val lines = mutableListOf<Line>()
fun add(ms: Int, level: Level, text: String) { lines.add(Line(ms, level, text)) }
fun dump(title: String) {
println(title)
lines.forEach { println(" %5d %-5s %s".format(it.ms, it.level, it.text)) }
}
}
// ── журнал «как получилось» ──
fun sloppy(): Journal = Journal().apply {
add(0, Level.DEBUG, "тут")
add(12, Level.DEBUG, "ok")
add(340, Level.DEBUG, "test")
add(1180, Level.ERROR, "ошибка")
add(1185, Level.DEBUG, "0")
}
// ── журнал по регламенту: [Тег_компонента]: Событие — Детали ──
fun regulated(): Journal = Journal().apply {
add(0, Level.INFO, "[CartScreen]: Экран создан — userId=42")
add(12, Level.INFO, "[CartScreen]: Нажата кнопка — «Обновить корзину»")
add(15, Level.INFO, "[CartRepository]: Запрос начат — GET /cart?userId=42")
add(1170, Level.DEBUG, "[CartRepository]: Ответ получен — HTTP 200, 486 байт")
add(1180, Level.ERROR, "[CartRepository]: Ошибка разбора — MissingFieldException: items; в теле пришло поле \"products\"")
add(1185, Level.INFO, "[CartScreen]: Состояние обновлено — items=0, error=null")
}
fun analyze(j: Journal, name: String) {
println("── что даёт «$name» ──")
val errors = j.lines.filter { it.level == Level.ERROR }
println(" всего строк: ${j.lines.size}, из них ERROR: ${errors.size}")
val tag = Regex("""^\[(\w+)]""")
val tagged = j.lines.count { tag.containsMatchIn(it.text) }
println(" строк, по которым понятно, ЧЕЙ это лог: $tagged")
val culprit = errors.firstOrNull()
val component = culprit?.let { tag.find(it.text)?.groupValues?.get(1) } ?: "неизвестно"
println(" компонент, где сломалось: $component")
val start = j.lines.firstOrNull { it.text.contains("Запрос начат") }
val delta = if (start != null && culprit != null) "${culprit.ms - start.ms} мс" else "посчитать нечем"
println(" время от запроса до ошибки: $delta")
val detail = culprit?.text?.substringAfter(" — ", "деталей нет") ?: "деталей нет"
println(" причина: $detail")
println()
}
fun main() {
val a = sloppy(); val b = regulated()
a.dump("── журнал А ──"); println()
b.dump("── журнал Б ──"); println()
analyze(a, "журнал А")
analyze(b, "журнал Б")
println("Сбой один и тот же: сервер ответил 200, но поле называется products, а не items.")
}
Живой вывод:
── что даёт «журнал А» ──
всего строк: 5, из них ERROR: 1
строк, по которым понятно, ЧЕЙ это лог: 0
компонент, где сломалось: неизвестно
время от запроса до ошибки: посчитать нечем
причина: деталей нет
── что даёт «журнал Б» ──
всего строк: 6, из них ERROR: 1
строк, по которым понятно, ЧЕЙ это лог: 6
компонент, где сломалось: CartRepository
время от запроса до ошибки: 1165 мс
причина: MissingFieldException: items; в теле пришло поле "products"
Строка ERROR есть в обоих журналах, и стоит она в нужном месте. Но по первому нельзя узнать ни компонент, ни время, ни причину — «строк, по которым понятно, чей это лог: 0».
По второму диагноз ставится за минуту: сломалось в CartRepository, через 1165 мс после запроса, сервер ответил 200, но поле называется products, а не items. То есть сеть была в порядке, а «пустая корзина» — опечатка в DTO.
Чек-лист перед сдачей сессии
Пять минут в конце, но только если всё делалось по ходу:
- Шапка есть в каждом новом файле — с описанием, датой
ДД-ММ-ГГГГи номером участника. - В каждом
catchестьLog.e— грепом по проекту:catchбез соседней строки с логом. - Ни одного
printlnв коде приложения. - Теги короче 23 символов — сборка это проверит, но лучше до неё.
- Ни одного
Logв теле@Composable— иначе журнал бесполезен. - Проект собирается. Последняя проверка после всех правок: не компилируется — не оценивается.
Типичные ошибки
Дата в другом формате. 2026-09-05 или 05.09.2026 вместо 05-09-2026. Ошибки нет, шапка выглядит заполненной, требование не закрыто.
println вместо Log. Строка видна, но не в Logcat.
Log в теле композабла. Журнал распухает в десятки раз.
catch без лога. Пустой блок, //TODO или e.printStackTrace(). Отладить нельзя, требование не закрыто.
Тег длиннее 23 символов. Сборка падает на проверке.
Комментарий-пересказ. // увеличиваем счётчик над counter++. Эксперт читает как отписку.
Всё отложено на последние полчаса. Правки веером по всем файлам, времени на проверку сборки нет.
Слова главы
- Множитель — оговорка «половина оценки», стоящая на большинстве аспектов ведомости.
- KDoc — комментарий-документация, начинается с
/**; первый абзац считается кратким описанием. - Шапка класса — описание, дата
ДД-ММ-ГГГГ, номер участника. Log.d/Log.i/Log.e— DEBUG, INFO, ERROR.- Тег — имя компонента в журнале, не длиннее 23 символов.
- Формат строки —
[Тег_компонента]: Событие — Детали. - Рекомпозиция — повторный вызов тела
@Composable; работы в нём быть не должно. - God ViewModel — одна модель на всё приложение.
- Ранний выход —
returnвместо углубления вложенности.
Тренажёр: набрать вслепую
Шапка и строки лога набираются на площадке десятки раз, а привозить заготовки нельзя.
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
/** Экран корзины: показывает товары и считает итог.
На чемпионате
Всё содержание этой главы проверяется, и почти всё — не отдельными аспектами, а множителем.
Порядок, который экономит баллы:
- Шапка пишется вместе с файлом, а не в конце. Полминуты на файл против часа паники.
Log.eпишется вместе сcatch— как часть конструкции, а не как то, что добавят потом.- Логи ставятся в обязательные точки сразу: открытие экрана, старт запроса, успех, ошибка, нажатие.
- Карточка списка ничего не считает — форматирование готовится в модели экрана.
- Проверка сборки после последней правки. Не компилируется — не оценивается, и это правило не знает исключений.
И отдельно про доску: канбан-дисциплина — второй множитель. Карточка, не переведённая в нужную колонку, означает, что задача не предъявлена к проверке. Двигать её нужно по ходу работы, а не проставлять задним числом.
Что дальше
Код приведён в вид, за который дают баллы, а не отнимают. Осталось то, что приложение делает руками пользователя и железом устройства: камера, уведомления, виджет — и то, чем работу проверяют: тесты. Эти темы впереди.
Углубиться
- Layout Inspector в Android Studio: колонки счётчиков композиций и пропусков — видно, какой участок перерисовывается зря.
- Синтаксис запросов Logcat:
tag:,level:,package:, отрицание через дефис — как отфильтровать журнал до пяти нужных строк. - KDoc целиком: блочные теги
@param,@return,@throws,@seeи что из этого видит редактор при наведении. - Detekt и ktlint — статические анализаторы, которые ловят длинные функции, глубокую вложенность и мёртвый код до эксперта.
- Timber — обёртка над
Log, которая сама подставляет тег и выключает отладочные записи в релизной сборке.
Сводная проверка по всей главе: 7 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Побочные эффекты в Compose — почему тело композабла должно быть свободно от побочных действий и куда их выносить.
- Logcat: запись и чтение журнала — уровни, теги и синтаксис запросов для фильтрации.
- Kotlin coding conventions — официальные соглашения по именованию и форматированию.
- Документирование кода на Kotlin (KDoc) — синтаксис и блочные теги.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.