Перейти к основному содержимому
Мобилка34ГЛАВА 34Чистый код: комментарии и логирование, которые оценивают/** ... */05-09-2026[Tag]: Событие×0,5 без них

Чистый код: комментарии и логирование, которые оценивают

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

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

Эта глава — про то, что легче всего пропустить и дороже всего пропускать.

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

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

Читать её стоит не в конце, а до того, как написан первый экран. Всё, что здесь описано, делается по ходу работы за минуты и почти неисполнимо за полчаса до сдачи.

Это множитель, а не критерий​

Разница между «отдельным критерием» и «множителем» решает всё.

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

ЭТО МНОЖИТЕЛЬ, А НЕ ОТДЕЛЬНЫЙ КРИТЕРИЙ149 аспектов ведомости101 аспект с оговоркой «половина оценки»48 безпод оговоркой — примерно 86 из 100 балловприложение сделано на 100%комментариев и логов нет57 балловприложение сделано на 65%комментарии и логи есть65 балловдвадцать шапок по полминуты — десять минут набора, которые спасают 43 балла
Оговорка про комментарии и логирование стоит на 101 аспекте из 149 — под ней примерно 86 из 100 баллов

Отсюда следствие, которое стоит принять до того, как начнётся сессия: комментарии и логирование — не то, что делают, если осталось время. Это то, без чего остальная работа стоит вдвое меньше.

Арифметика: почему аккуратный середняк обгоняет идеалиста​

Посчитаем на модели. Заодно она проверяет пять типовых шапок класса на соответствие требованию — и показывает, какие из них не проходят.

// Модель приёмки: эксперт смотрит шапку класса и ищет три вещи — // описание, дату создания ДД-ММ-ГГГГ и номер участника. Проверим пять реальных шапок. 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. Формат проверяется буквально.

Отвечено вопросов: 0 из 2
Чем множитель отличается от отдельного критерия?
Дата создания класса записана как 2026-09-05. Это пройдёт?
Отвечено верно: 0 из 2

Почему код в теле @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
ОДНА РАБОТА ПОЛЬЗОВАТЕЛЯ, 30 ВЫЗОВОВ ТЕЛАлогика в теле @Composable30 проходов фильтра · 900 созданных объектов форматтералогика вынесена, тело только рисует1 проход фильтра · 1 объект форматтерато же число рекомпозиций — но работы в них больше нет«логика не в разметке» — не вкусовщина, а разница в девятьсот раз
Число рекомпозиций одинаково; разница в том, сколько работы делается внутри каждой

Девятьсот созданных объектов против одного — при абсолютно одинаковом результате на экране. Вот почему «логика не живёт в разметке» — не вопрос вкуса и не совет по стилю.

Последние две строки вывода — отдельная ловушка, на которой спотыкаются все. Переменная, объявленная внутри тела, пересоздаётся при каждом вызове. Отсюда классическая жалоба: «нажимаю пять раз, а счётчик показывает единицу». Состояние обязано жить снаружи — в 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)
}
Отвечено вопросов: 0 из 2
По какому признаку разрезают длинную функцию?
Почему Log.d прямо в теле @Composable — плохая идея?
Отвечено верно: 0 из 2

Шапка класса по формату задания​

Требование конкурсного задания дословно: для каждого создаваемого класса верхнего уровня в коде должны присутствовать структурированные комментарии, включающие описание назначения класса, дату создания в формате ДД-ММ-ГГГГ и номер участника.

/**
* Экран корзины: показывает выбранные товары, считает итог
* и отправляет заказ через 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"
ОДИН И ТОТ ЖЕ СБОЙ, ДВА ЖУРНАЛАкак получилосьDEBUG тутDEBUG okDEBUG testERROR ошибкаDEBUG 0компонент неизвестен · причины нетпо регламентуINFO [CartScreen]: Экран созданINFO [CartRepository]: Запрос начатDEBUG [CartRepository]: HTTP 200ERROR [CartRepository]: Ошибка разбора — пришло поле "products"компонент · 1165 мс · причинасбой один: сервер ответил 200, но поле называется products, а не itemsслева это не найти в принципе — не из чего; справа читается механически
Сбой один и тот же; по левому журналу причину найти нельзя в принципе, по правому она читается механически

Строка ERROR есть в обоих журналах, и стоит она в нужном месте. Но по первому нельзя узнать ни компонент, ни время, ни причину — «строк, по которым понятно, чей это лог: 0».

По второму диагноз ставится за минуту: сломалось в CartRepository, через 1165 мс после запроса, сервер ответил 200, но поле называется products, а не items. То есть сеть была в порядке, а «пустая корзина» — опечатка в DTO.

Отвечено вопросов: 0 из 2
Почему println не заменяет Log.d?
Тег [ProductDetailsScreenViewModel] — что с ним не так?
Отвечено верно: 0 из 2

Чек-лист перед сдачей сессии​

Пять минут в конце, но только если всё делалось по ходу:

  1. Шапка есть в каждом новом файле — с описанием, датой ДД-ММ-ГГГГ и номером участника.
  2. В каждом catch есть Log.e — грепом по проекту: catch без соседней строки с логом.
  3. Ни одного println в коде приложения.
  4. Теги короче 23 символов — сборка это проверит, но лучше до неё.
  5. Ни одного Log в теле @Composable — иначе журнал бесполезен.
  6. Проект собирается. Последняя проверка после всех правок: не компилируется — не оценивается.

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

Дата в другом формате. 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%

/** Экран корзины: показывает товары и считает итог.

На чемпионате​

Всё содержание этой главы проверяется, и почти всё — не отдельными аспектами, а множителем.

Порядок, который экономит баллы:

  1. Шапка пишется вместе с файлом, а не в конце. Полминуты на файл против часа паники.
  2. Log.e пишется вместе с catch — как часть конструкции, а не как то, что добавят потом.
  3. Логи ставятся в обязательные точки сразу: открытие экрана, старт запроса, успех, ошибка, нажатие.
  4. Карточка списка ничего не считает — форматирование готовится в модели экрана.
  5. Проверка сборки после последней правки. Не компилируется — не оценивается, и это правило не знает исключений.

И отдельно про доску: канбан-дисциплина — второй множитель. Карточка, не переведённая в нужную колонку, означает, что задача не предъявлена к проверке. Двигать её нужно по ходу работы, а не проставлять задним числом.

Отвечено вопросов: 0 из 2
Когда писать шапку класса?
В catch стоит e.printStackTrace(). Требование закрыто?
Отвечено верно: 0 из 2

Что дальше​

Код приведён в вид, за который дают баллы, а не отнимают. Осталось то, что приложение делает руками пользователя и железом устройства: камера, уведомления, виджет — и то, чем работу проверяют: тесты. Эти темы впереди.

Отвечено вопросов: 0 из 1
Девятьсот созданных объектов против одного — откуда такая разница при одинаковом экране?
Отвечено верно: 0 из 1

Углубиться​

  • Layout Inspector в Android Studio: колонки счётчиков композиций и пропусков — видно, какой участок перерисовывается зря.
  • Синтаксис запросов Logcat: tag:, level:, package:, отрицание через дефис — как отфильтровать журнал до пяти нужных строк.
  • KDoc целиком: блочные теги @param, @return, @throws, @see и что из этого видит редактор при наведении.
  • Detekt и ktlint — статические анализаторы, которые ловят длинные функции, глубокую вложенность и мёртвый код до эксперта.
  • Timber — обёртка над Log, которая сама подставляет тег и выключает отладочные записи в релизной сборке.

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

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

Видео по теме

ВидеоУрок 17: Жизненный цикл Activity, Logcat (логирование)Kotlin & Android с нуля
ВидеоKotlin Code Style за 20 минутFunDroid
ВидеоЧистый код / Clean Code #2: Комментарии, условные выраженияEngineerSpock
ВидеоJetpack Compose: Debugging recompositionAndroid Developers
ВидеоWhere to hoist that state in Compose?Android Developers

Комментарии

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