1. Вступ
Коли програма маленька, спокуса зберігати все в одному файлі .kt величезна. Це як «одна коробка для всього»: і шкарпетки, і зарядки, і документи — доки коробка маленька, ніби й зручно. Але щойно код зростає, коробка перетворюється на чорну діру: ви точно памʼятаєте, що десь була функція readIntOrNull(), але де саме — уже ні.
Пакети (package) у Kotlin — це спосіб дати коду зрозумілу «адресу», щоб ви (і IDE) могли швидко знаходити потрібні функції, а однакові імена не «змагалися» між собою за право жити в одному просторі.
Важливо одразу вловити ключову думку:
package — це «простір імен» (namespace), логічне групування коду.
Папки в проєкті — це вже «фізична організація» файлів на диску. Зазвичай вони узгоджені, але це різні речі.
2. Що таке package: простір імен і «адреса» оголошень
Якщо дивитися на Kotlin-файл як на лист, то package — це рядок «куди доставити». На початку файлу ви пишете:
package app
І все, що оголошено в цьому файлі (функції, константи тощо), починає «жити» за адресою app.
Мінімальний приклад: файл у пакеті app
Уявімо, що ми робимо консольну гру «Вгадай число» (з міні‑проєкту). Поки все просто, у нас може бути один файл Main.kt:
package app
fun main() {
println("Гру розпочато!") // Гру розпочато!
}
Тепер main формально має «повне імʼя» (fully qualified name): app.main. Зазвичай його так не пишуть, але мислити так корисно.
Один файл — один package
У Kotlin package задається для всього файлу. Ви не можете в одному файлі .kt оголосити половину функцій у app, а половину — у app.util. Якщо потрібен інший простір імен — створюємо інший файл.
Оголошення верхнього рівня «належать» пакету
Ви вже бачили, що змінні та функції в Kotlin можна оголошувати на верхньому рівні файлу (поза класами). Так от: такі оголошення теж живуть у пакеті файлу. Пакет — це «дім» для оголошень, а файл — радше «контейнер» або «аркуш паперу», на якому ви ці оголошення записали.
3. Повне імʼя: як Kotlin розрізняє однакові імена
Коли проєкт зростає, ви майже неминуче напишете дві функції з однаковим «коротким» імʼям. Наприклад, format() — чудове імʼя, і його хочеться використовувати в різних місцях.
З погляду Kotlin унікальність імені забезпечується не лише «іменем функції», а поєднанням:
package + імʼя
(і ще сигнатурою, але це вже окрема історія).
Дві однакові функції в різних пакетах — це нормально
Файл 1: src/main/kotlin/app/util/format.kt
package app.util
fun formatScore(score: Int): String {
return "Рахунок: $score"
}
Файл 2: src/main/kotlin/app/cli/format.kt
package app.cli
fun formatPrompt(text: String): String {
return ">> $text"
}
Обидві функції називаються по-різному (formatScore і formatPrompt), але навіть якби вони обидві називалися просто format, Kotlin однаково зміг би їх розрізнити за пакетами. Проблема почнеться не в «існуванні», а у «використанні» — коли ви спробуєте викликати format() із третього місця. Це ми будемо акуратно розвʼязувати через import у наступній лекції, а сьогодні нам важливо зрозуміти сам принцип адресації.
Важливий нюанс: файл не є областю видимості для розвʼязання імен
Цей момент звучить як фраза з документації про компілятор, але він справді практичний. Kotlin (і IDE) намагаються зробити так, щоб перенесення функції в інший файл того самого пакета не ламало код лише тому, що «вона тепер в іншому файлі». У специфікації розвʼязання імен прямо обговорюється ідея, що «немає окремої області видимості файлу» — важливий пакет, а не конкретний .kt.
Пояснення людською мовою: файл — це не «власник» функції. Власник — пакет. Тому пакет можна уявляти як «папку в телефонній книзі»: не важливо, на якій сторінці записано номер, важливо, у якій категорії він лежить.
4. Папки та package: домовленість, яка рятує нерви
Далі — важлива практична частина: як це зазвичай організовують проєкт на Kotlin/JVM.
У Gradle‑проєкті (а в більшості з вас він саме такий) код зазвичай лежить тут:
src/main/kotlin/
А далі починається найцікавіше: всередині src/main/kotlin зазвичай створюють папки, які повторюють package.
Відповідність «крапки ↔ папки»
Є просте правило-асоціація:
| У package | На диску |
|---|---|
|
|
|
|
|
|
|
|
Тобто:
- крапки в package перетворюються на «папки»;
- порядок папок повторює «адресу».
Наприклад, якщо ви пишете:
package app.util
то файл зазвичай лежить за шляхом:
src/main/kotlin/app/util/ІмʼяФайла.kt
Чому це саме «домовленість», а не жорсткий закон
Компілятор Kotlin технічно може зібрати проєкт, у якому файл лежить у «не тій» папці (особливо якщо ви вручну налаштували вихідні каталоги). Але IDE і люди від цього страждатимуть.
IntelliJ IDEA вміє підсвічувати проблему приблизно так: Package directive doesn't match file location. І це той рідкісний випадок, коли IDE не занудствує, а справді намагається врятувати вас від майбутнього хаосу.
Аналогія: адреса і склад
package — це «адреса на конверті» (як вас знайти логічно). Папки — це «розташування коробок на складі» (як це зберігається фізично).
Якщо адреса й склад збігаються — ви щасливі. Якщо не збігаються — ви все ще можете знайти коробку, але робитимете це як герой квесту: «принеси артефакт із підвалу, але мапа в тебе від іншої гри».
5. Практика на рівні проєкту: рознесення гри «Вгадай число» за пакетами
Зараз ми не додаватимемо нові мовні конструкції. Натомість візьмемо вже знайомі функції й просто розкладемо їх за смисловими «адресами».
Уявімо, що в нас є логіка:
- введення числа,
- генерація секрету,
- основний ігровий цикл,
- виведення повідомлень користувачеві.
Якщо тримати все в одному файлі Main.kt, він швидко роздується. Тому поділимо на пакети:
- app — точка входу й складання сценарію (тобто main)
- app.game — логіка гри
- app.io — введення та валідація (утиліти читання)
- app.util — дрібні помічники для форматування
Намалюємо це як «мапу адрес»:
flowchart TD
A["package app
Main.kt"] --> B["package app.game
Game.kt"]
A --> C["package app.io
Input.kt"]
A --> D["package app.util
Text.kt"]
app: точка входу
Файл: src/main/kotlin/app/Main.kt
package app
fun main() {
println("Ласкаво просимо до 'Вгадай число'!") // Ласкаво просимо до 'Вгадай число'!
}
Поки тут небагато. Але ідея така: main — це «диригент». Він викликає функції з інших пакетів і поєднує сценарій в один застосунок.
app.util: нормалізація тексту
Файл: src/main/kotlin/app/util/Text.kt
package app.util
fun normalizeCommand(raw: String): String {
return raw.trim().lowercase()
}
Ця функція маленька, але дуже «пакетна»: вона не про гру й не про введення чисел — вона про обробку рядків загалом. Отже, їй логічно жити в util.
app.io: читання числа «по-людськи»
Файл: src/main/kotlin/app/io/Input.kt
package app.io
fun readIntOrNull(prompt: String): Int? {
print(prompt)
val raw = readln().trim()
return raw.toIntOrNull()
}
Зверніть увагу: тут немає жодної магії. Це та сама логіка, яку ви писали раніше, просто тепер вона в окремому файлі й має зрозумілу адресу app.io.readIntOrNull.
app.game: генерація секрету
Файл: src/main/kotlin/app/game/Secret.kt
package app.game
import kotlin.random.Random
fun generateSecret(from: Int, until: Int): Int {
return Random.nextInt(from, until)
}
Тут ми знову використовуємо вже знайому тему Random (ви її проходили раніше). Важливо, що логіка, повʼязана з грою, починає жити в app.game. Це робить проєкт читабельнішим: ви буквально за шляхом файлу розумієте, «про що він».
6. Дисципліна пакетів: default package і найменування
Пакет за замовчуванням і чому це майже завжди погана ідея
Іноді ви бачите файл без рядка package на початку. Це означає, що файл перебуває в пакеті за замовчуванням (default package).
Технічно це працює. Практично — це як «жити без адреси»: курʼєр усе одно може вам подзвонити й запитати: «Ну ви десь біля підʼїзду, так?», але нормальне життя так не влаштоване.
Основні проблеми default package зазвичай проявляються не одразу. Спочатку все добре: один файл, другий файл, третій… А потім ви раптом хочете:
- поділити код на підпакети,
- повторно використати функції в іншому проєкті,
- зрозуміти, де й за що відповідає код,
- уникнути конфліктів імен.
І ось тут відсутність пакетів починає мститися.
Гарне правило для навчальних і маленьких проєктів: щойно у вас більше одного файлу — заведіть нормальний package, хоча б package app.
Та й для одного файлу це часто корисно: ви звикаєте до дисципліни й менше страждаєте, коли проєкт зростає.
Нюанси найменування пакетів
В індустрії є загальні домовленості:
Імена пакетів пишуть у нижньому регістрі, зазвичай через крапки — наприклад, app.util або com.example.myapp. Такий запис зручний і читабельний, а ще він майже не конфліктує з іменами класів і функцій.
Якщо дуже хочеться назвати пакет MyCoolStuff, зупиніться на секунду й уявіть, як ви житимете з цим за пів року. Найімовірніше, ви все одно прийдете до mycoolstuff або my.cool.stuff. Kotlin‑код читається багато разів, а пишеться один раз — тож краще одразу вибрати варіант, який не дратує око.
Ще один момент: пакет — це логічне групування. Не намагайтеся зробити «пакет на кожну функцію». Це як у супермаркеті: якщо у вас окрема полиця для кожного батона, ви не впорядкували магазин — ви побудували музей батонів.
7. Типові помилки під час роботи з package і папками
Помилка № 1: package не відповідає розташуванню файлу в src.
Це найпоширеніша історія: файл лежить у src/main/kotlin/app/util, а на початку написано package app.utils (зайва s) або взагалі package app. Формально ви навіть можете якийсь час жити так, але IDE плутатиметься, навігація стане дивною, а проєкт почне нагадувати шафу, де футболки лежать у шухляді «ложки».
Помилка № 2: усе складається в один пакет «бо так простіше».
Пакет app швидко перетворюється на «пакет‑смітник», де живе і введення, і логіка гри, і форматування. Спочатку це здається зручним, а потім ви боїтеся відкривати автодоповнення, бо там 30 функцій зі схожими іменами. Краще додавати підпакети за змістом: app.io, app.game, app.util.
Помилка № 3: очікування, що «один файл = один пакет».
Навпаки: один пакет майже завжди складається з багатьох файлів. Пакет — логічна область, а файл — просто місце, де ви записали частину оголошень. Важливо звикнути мислити пакетами, а не файлами. Інакше ви будете «шукати функцію за назвою файлу» — це не найприємніша форма археології.
Помилка № 4: залишати файли в default package «доки проєкт маленький».
Це пастка. Проєкт маленький рівно доти, доки ви не додали другий файл і не захотіли трохи порядку. А далі починається міграція: перенесення файлів, лагодження посилань, «переїзд» каталогів. Значно дешевше завести нормальний пакет одразу, навіть якщо він один.
Помилка № 5: дивні імена пакетів (пробіли, верхній регістр, «як попало»).
Технічно деякі речі навіть можуть зібратися (до першого серйозного конфлікту), але читабельність і культура проєкту падають. Нижній регістр і зрозумілі слова — нудно, зате працює. Як і більшість хороших інженерних рішень.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ