Що ви опануєте
Ви навчитеся створювати Kotlin моделі, перевіряти введення, працювати з колекціями та передавати поведінку через lambda. Ці знання продовжують першу лекцію про Android і готують до Compose екранів. Наскрізний приклад — перелік навчальних завдань із позначенням виконання. Кожне правило спочатку пояснимо звичайною функцією без залежності від UI.
Типи та оголошення
val задає посилання або значення, якому не можна звичайно присвоїти інший результат; var дозволяє переприсвоєння. Для посилального об’єкта val не означає глибоку незмінність його вмісту. Статичний тип перевіряється compiler. Int містить ціле число, Double — наближене дробове, Boolean — логічне, String — текст.
val count = 3 виводить тип Int. Явний тип корисний на межі API й там, де потрібно пояснити контракт. Вираз 5 / 2 з Int дає 2; для дробового результату потрібно 5.0 / 2. Переповнення Int у JVM не є правильною реалізацією предметного діапазону, тому перед обчисленням великих значень перевіряйте межі або використовуйте відповідний тип.
Nullable та перевірка введення
String? може містити текст або null. Safe call ?. виконує доступ лише за наявності значення. Elvis ?: задає результат для null. !! стверджує ненульовість і спричиняє помилку, якщо припущення хибне. Введення користувача є зовнішніми даними, тому його потрібно перевіряти, а не довільно примушувати до потрібного типу.
fun parseMinutes(raw: String): Int? {
val value = raw.trim().toIntOrNull() ?: return null
return value.takeIf { it in 1..240 }
}
fun main() {
println(parseMinutes(" 30 ")) // 30
println(parseMinutes("abc")) // null
println(parseMinutes("0")) // null
}
trim прибирає крайні пробіли. toIntOrNull відхиляє нечисловий текст і значення поза діапазоном Int. Раннє return завершує функцію. takeIf залишає значення, якщо lambda повертає true; it є її параметром. Діапазон 1..240 включає обидві границі. Null тут означає непридатне введення за погодженим контрактом.
Функції та вирази
Функція має параметри й тип результату. Kotlin дозволяє expression body, коли результат визначає один вираз. if і when можуть повертати значення. Параметри за замовчуванням роблять виклик коротшим, але повинні відповідати очікуваній поведінці. Іменовані аргументи допомагають читати кілька однотипних параметрів.
fun progressLabel(done: Int, total: Int): String {
require(total >= 0 && done in 0..total) { "Некоректний прогрес" }
return when {
total == 0 -> "Завдань ще немає"
done == total -> "Усе виконано"
else -> "Виконано $done із $total"
}
}
require перевіряє контракт аргументів і кидає IllegalArgumentException. Перший branch обробляє порожній набір до порівняння завершення. Інтерполяція підставляє значення в рядок. Зовнішній UI повинен мати політику для помилок введення; require підходить для порушення контракту функції, а не для кожного натискання користувача.
Data class та копія стану
Data class автоматично надає корисні операції для даних, зокрема equals, toString і copy на основі властивостей primary constructor. Властивості val роблять переприсвоєння забороненим, але вкладені змінювані об’єкти все ще потребують власної дисципліни. Copy є поверхневим для таких посилань.
data class StudyTask(val id: Int, val title: String, val completed: Boolean = false)
val original = StudyTask(1, "Kotlin")
val updated = original.copy(completed = true)
Original залишається completed=false, updated має true. Для Compose така заміна простого immutable стану робить зміни видимими через відповідний observable holder. Data class сам по собі не запускає recomposition; потрібно змінити стан, який UI спостерігає.
Колекції та lambda
List<T> надає інтерфейс читання впорядкованих елементів, MutableList<T> — зміни. Read-only інтерфейс не гарантує глибоку незмінність джерела. map перетворює кожен елемент, filter відбирає, count рахує за умовою. Lambda описує передану поведінку й може захоплювати значення зовнішньої області.
val tasks = listOf(StudyTask(1, "Kotlin"), StudyTask(2, "Compose", true))
val completedTitles = tasks.filter { it.completed }.map { it.title }
val next = tasks.map { if (it.id == 1) it.copy(completed = true) else it }
Фрагмент створює нові колекції й не змінює початковий List. ID пов’язує подію з предметним елементом. Пошук за позицією може стати неправильним після сортування або фільтрації, тому стабільна ідентичність важлива для UI списків.
Типові помилки та практика
Помилки: !! для неперевіреного введення, Int ділення для частки, плутанина val із незмінністю всього об’єкта й зміна MutableList без observable повідомлення. Налагоджуйте чисту функцію в Kotlin scratch або модульному тесті до включення в Android screen. Запишіть звичайні, граничні й помилкові значення.
Практичний аналіз
Для списку завдань визначте правило перейменування й перевірте порожню назву. Поясніть, як map створить новий стан за ID. Який тип результату парсингу потрібний, якщо UI має розрізняти нечисловий текст і число поза діапазоном? Таке розширення повинно додати явні причини відмови.
Як Kotlin допомагає описувати допустимі дані
Тип String? дозволяє відсутність тексту, а String очікує наявне значення. Це впливає на дозволені операції: прямий доступ до nullable-значення потребує перевірки або оператора безпечного виклику. Elvis-оператор ?: задає результат, коли ліва частина null. Підставляйте значення лише тоді, коли воно має предметний зміст, бо випадковий нуль може приховати помилку введення.
ToIntOrNull повертає null, якщо текст не представляє допустиме int. Після успішного перетворення все одно потрібна перевірка діапазону: число −5 є коректним int, але може бути некоректною тривалістю. Технічна й предметна валідність є послідовними кроками обробки. Повідомлення користувачу повинне пояснювати, який крок не пройдено.
Data class генерує операції для значущих властивостей основного конструктора. Copy створює новий екземпляр із заданими змінами, але вкладені змінювані колекції можуть залишатися спільними. Якщо стан інтерфейсу містить List, використовуйте узгоджену політику незмінності та створення нової колекції. Val забороняє переприсвоєння змінної, але не обов’язково зміну об’єкта, на який вона посилається.
Лямбда описує передану поведінку. Callback onSubmit передає подію від UI до власника логіки; параметри повинні представляти достатні дані для рішення. Якщо callback отримує вже перевірену тривалість, поясніть, де гарантується ця передумова. Якщо отримує сирий текст, логіка повинна виконати перетворення й повернути стан помилки. Так сигнатура допомагає читати межі відповідальності.
Схема процесу
Перевіряємо зовнішній текст.
Самоперевірка
Як обґрунтувати відповідь своїми словами?
Підсумок
Kotlin типи, null safety та функції роблять контракти видимими. Data class і колекційні операції допомагають представляти новий стан без прихованих змін. Observable UI потребує окремого повідомлення про цей стан.