🎯 Kotlin

Module, Pakete und Importe in Kotlin: Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы собираете огромный конструктор LEGO. У вас есть коробка с тысячами деталей. Если вы будете искать нужную деталь каждый раз, перебирая всю кучу, вы потратите часы. Гораздо удобнее разложить детали по маленьким коробочкам: в одной — колёса, в другой — кирпичики 2×4, в третьей — окошки и двери. Когда вам понадобится собрать колёсную базу, вы просто берёте коробочку с нужными деталями.

Точно так же работает и программирование. Когда программа становится большой, хранить весь код в одном файле — это как держать все детали LEGO в одной огромной корзине. Вы быстро запутаетесь, а если нужно что-то исправить, придётся перечитывать тысячи строк.

Модули и пакеты в Kotlin — это те самые коробочки для вашего кода. Они помогают:

  • Наводить порядок: код разбивается на логические части (например, отдельно работа с базой данных, отдельно — интерфейс пользователя).
  • Избегать путаницы: если в разных частях программы есть функции с одинаковыми именами, они не будут конфликтовать, если лежат в разных пакетах.
  • Переиспользовать код: написали однажды полезную функцию — положили в модуль и используете в сотне проектов.
  • Ускорять компиляцию: если вы изменили только один модуль, компилятору не нужно перепроверять весь проект.
💡 Важно: В Kotlin понятия «модуль» и «пакет» немного отличаются. Модуль — это более крупная единица (например, целая библиотека или часть проекта). Пакет — это просто папка с файлами внутри модуля. Но для простоты мы будем говорить о них как о способе организации кода.

Теория: как это работает изнутри

Давайте заглянем «под капот» и разберёмся, как Kotlin обрабатывает модули и импорты.

Пакеты — это пространства имён

Когда вы пишете package ru.mysite.utils, вы говорите компилятору: «Всё, что написано в этом файле, принадлежит к группе ru.mysite.utils». Это как фамилия у людей. Два человека могут иметь имя «Алексей», но если у одного фамилия «Иванов», а у другого — «Петров», их не перепутают. Так и с функциями: ru.mysite.utils.print() и ru.mysite.ui.print() — это разные функции, хотя называются одинаково.

Как Kotlin находит нужный код

Когда вы пишете import ru.mysite.utils.println, компилятор:

  1. Смотрит на имя пакета (ru.mysite.utils).
  2. Идёт в соответствующую папку в проекте (папки ru/mysite/utils).
  3. Ищет там файл с объявлением функции println.
  4. Делает эту функцию доступной для использования в вашем файле.

Если вы не указали импорт, Kotlin всё равно автоматически импортирует несколько базовых пакетов (например, kotlin.*, kotlin.io.*). Именно поэтому вы можете писать println() без всяких импортов — эта функция уже «подключена» по умолчанию.

Модули — это проекты или библиотеки

Модуль — это более крупная единица, чем пакет. В типичном проекте модулем может быть:

  • Основное приложение (app).
  • Библиотека для работы с сетью (network).
  • Библиотека с общими утилитами (common).

Каждый модуль имеет свой файл сборки (build.gradle.kts), свои зависимости и может быть скомпилирован отдельно. Модули могут зависеть друг от друга, но не могут иметь циклических зависимостей (когда модуль A зависит от B, а B — от A).

⚠️ Важное правило: В одном файле Kotlin может быть только одно объявление пакета (в начале файла). Но в одном пакете может быть сколько угодно файлов.

Синтаксис в Kotlin: все формы и варианты

Давайте разберём все возможные способы работы с импортами и пакетами в Kotlin.

1. Объявление пакета

Пишется в самом начале файла (до любых других инструкций):

package com.example.myapp.utils

// Весь код файла принадлежит этому пакету
fun sayHello() = println("Привет из utils!")

2. Импорт одного элемента

import com.example.myapp.utils.sayHello

fun main() {
    sayHello()  // Теперь можно вызывать без указания пакета
}

3. Импорт всего пакета (звёздочка)

import com.example.myapp.utils.*  // Импортируем всё из пакета

fun main() {
    sayHello()
    doSomething()  // Если эта функция тоже есть в utils
}
💡 Совет: Импорт со звёздочкой удобен, но может привести к конфликтам имён. Лучше импортировать только то, что реально используете.

4. Импорт с псевдонимом (alias)

Если имена конфликтуют, можно дать импорту другое имя:

import com.example.utils.print as utilsPrint
import com.example.ui.print as uiPrint

fun main() {
    utilsPrint("Это из утилит")
    uiPrint("Это из интерфейса")
}

5. Импорт вложенных классов

import com.example.model.User  // Импортируем класс User
import com.example.model.User.Address  // Импортируем вложенный класс Address

fun main() {
    val user = User("Анна")
    val address = Address("Москва")
}

6. Импорт top-level функций и свойств

В Kotlin функции и свойства могут быть объявлены прямо в файле (не внутри класса). Их тоже можно импортировать:

// Файл: Constants.kt
package com.example.config

val APP_NAME = "Моё приложение"
fun getVersion() = "1.0.0"
// Другой файл
import com.example.config.APP_NAME
import com.example.config.getVersion

fun main() {
    println(APP_NAME)       // "Моё приложение"
    println(getVersion())   // "1.0.0"
}

7. Импорт extension-функций

Extension-функции (функции-расширения) тоже нужно импортировать:

// Файл: StringExtensions.kt
package com.example.extensions

fun String.addExclamation() = this + "!"  // Расширение для String
// Другой файл
import com.example.extensions.addExclamation

fun main() {
    val text = "Привет"
    println(text.addExclamation())  // "Привет!"
}

8. Импорт операторов (infix функций)

// Файл: MathUtils.kt
package com.example.math

infix fun Int.plus(x: Int) = this + x  // Инфиксная функция
import com.example.math.plus

fun main() {
    val result = 5 plus 3  // Используем как оператор
    println(result)  // 8
}

Примеры кода — от простого к сложному

Пример 1: Создание простого пакета

// Файл: greetings.kt (пакет: com.example.greetings)
package com.example.greetings

// Простая функция приветствия
fun sayHello(name: String): String {
    return "Привет, $name!"
}

// Ещё одна функция
fun sayGoodbye(name: String): String {
    return "До свидания, $name!"
}
// Файл: main.kt
import com.example.greetings.sayHello
import com.example.greetings.sayGoodbye

fun main() {
    val hello = sayHello("Мария")  // Вызываем импортированную функцию
    println(hello)  // Выведет: Привет, Мария!
    
    val bye = sayGoodbye("Мария")
    println(bye)    // Выведет: До свидания, Мария!
}

Объяснение: Мы создали два файла в разных пакетах. В main.kt мы импортировали только те функции, которые нам нужны, и использовали их как обычные.

Пример 2: Импорт всего пакета и конфликт имён

// Файл: utils.kt (пакет: com.example.utils)
package com.example.utils

fun printMessage(msg: String) = println("Utils: $msg")
// Файл: ui.kt (пакет: com.example.ui)
package com.example.ui

fun printMessage(msg: String) = println("UI: $msg")
// Файл: main.kt
import com.example.utils.*
import com.example.ui.*

fun main() {
    // printMessage("Тест")  // ОШИБКА! Конфликт имён - непонятно, какую вызывать
}

Решение: Используем псевдонимы или явные вызовы:

import com.example.utils.printMessage as utilsPrint
import com.example.ui.printMessage as uiPrint

fun main() {
    utilsPrint("Тест")  // Выведет: Utils: Тест
    uiPrint("Тест")     // Выведет: UI: Тест
}

Пример 3: Работа с классами из разных пакетов

// Файл: models.kt (пакет: com.example.models)
package com.example.models

data class User(val name: String, val age: Int)
data class Product(val name: String, val price: Double)
// Файл: services.kt (пакет: com.example.services)
package com.example.services

import com.example.models.User  // Импортируем класс из другого пакета

class UserService {
    fun createUser(name: String, age: Int): User {
        return User(name, age)  // Используем импортированный класс
    }
    
    fun isAdult(user: User): Boolean {
        return user.age >= 18
    }
}
// Файл: main.kt
import com.example.models.Product
import com.example.services.UserService

fun main() {
    val service = UserService()
    val user = service.createUser("Иван", 25)
    println("Пользователь: ${user.name}, возраст: ${user.age}")
    println("Совершеннолетний? ${service.isAdult(user)}")
    
    val product = Product("Ноутбук", 75000.0)
    println("Товар: ${product.name}, цена: ${product.price}")
}

Пример 4: Использование extension-функций из другого пакета

// Файл: extensions.kt (пакет: com.example.extensions)
package com.example.extensions

// Расширение для Int: проверка, является ли число чётным
fun Int.isEven(): Boolean = this % 2 == 0

// Расширение для String: повторение строки n раз
fun String.repeat(n: Int): String {
    val sb = StringBuilder()
    for (i in 1..n) {
        sb.append(this)
    }
    return sb.toString()
}
// Файл: main.kt
import com.example.extensions.isEven
import com.example.extensions.repeat

fun main() {
    val number = 42
    println("$number чётное? ${number.isEven()}")  // true
    
    val text = "Kotlin "
    println(text.repeat(3))  // "Kotlin Kotlin Kotlin "
}

Пример 5: Работа с вложенными пакетами

// Файл: database/connection.kt (пакет: com.example.database)
package com.example.database

class DatabaseConnection {
    fun connect() = println("Подключение к БД...")
    fun disconnect() = println("Отключение от БД...")
}
// Файл: database/repository/user.kt (пакет: com.example.database.repository)
package com.example.database.repository

import com.example.database.DatabaseConnection  // Импорт из родительского пакета

class UserRepository {
    private val connection = DatabaseConnection()
    
    fun saveUser(name: String) {
        connection.connect()
        println("Сохранение пользователя $name")
        connection.disconnect()
    }
}
// Файл: main.kt
import com.example.database.repository.UserRepository

fun main() {
    val repo = UserRepository()
    repo.saveUser("Анна")
}

Пример 6: Импорт статических членов (companion object)

// Файл: config.kt (пакет: com.example.config)
package com.example.config

class AppConfig {
    companion object {
        const val APP_NAME = "MyApp"
        const val VERSION = "2.0.0"
        fun getFullName() = "$APP_NAME v$VERSION"
    }
}
// Файл: main.kt
import com.example.config.AppConfig  // Импортируем класс
// import com.example.config.AppConfig.Companion  // Можно и так

fun main() {
    println(AppConfig.APP_NAME)      // "MyApp"
    println(AppConfig.VERSION)       // "2.0.0"
    println(AppConfig.getFullName()) // "MyApp v2.0.0"
}

Пример 7: Создание своей библиотеки (модуля)

// Файл: build.gradle.kts для модуля "mylibrary"
plugins {
    kotlin("jvm")
}

dependencies {
    // Здесь могут быть зависимости
}
// Файл: src/main/kotlin/com/example/mylibrary/MathUtils.kt
package com.example.mylibrary

object MathUtils {
    fun factorial(n: Int): Long {
        if (n <= 1) return 1
        return n * factorial(n - 1)
    }
    
    fun fibonacci(n: Int): Long {
        if (n <= 1) return n.toLong()
        var a = 0L
        var b = 1L
        for (i in 2..n) {
            val temp = a + b
            a = b
            b = temp
        }
        return b
    }
}
// Файл: build.gradle.kts основного модуля
dependencies {
    implementation(project(":mylibrary"))  // Подключаем нашу библиотеку
}
// Файл: main.kt в основном модуле
import com.example.mylibrary.MathUtils

fun main() {
    println("Факториал 5: ${MathUtils.factorial(5)}")    // 120
    println("Фибоначчи 10: ${MathUtils.fibonacci(10)}")  // 55
}

Сравнение с Python: чем похоже и чем отличается

Аспект Kotlin Python
Объявление пакета/модуля package com.example.utils в начале файла Файл utils.py автоматически становится модулем utils
Импорт одного элемента import com.example.utils.sayHello from utils import say_hello
Импорт всего import com.example.utils.* from utils import *
Псевдонимы import ... as utilsPrint import utils as u
Обязательность пакета Можно не указывать (тогда файл в «корневом» пакете) Модуль — это любой .py файл, пакет — папка с __init__.py
Импорт по умолчанию kotlin.*, kotlin.io.* и др. Только встроенные функции (print, len и т.д.)
Циклические импорты Запрещены на уровне модулей Можно, но часто приводит к ошибкам
Импорт extension-функций Да, нужно явно импортировать Нет аналога (в Python нет extension-функций)
💡 Главное отличие: В Python модуль — это просто файл, а пакет — папка с файлом __init__.py. В Kotlin пакет объявляется декларативно через package, и структура папок не обязана совпадать с именем пакета (хотя так делать настоятельно рекомендуется).

Типичные ошибки и подводные камни

Ошибка 1: Забыли указать импорт

// Ошибка!
fun main() {
    sayHello()  // Unresolved reference: sayHello
}

Решение: Добавьте import com.example.utils.sayHello в начало файла.

Ошибка 2: Конфликт имён при импорте со звёздочкой

import com.example.utils.*
import com.example.ui.*

fun main() {
    print("Тест")  // Какой print использовать? Ошибка компиляции!
}

Решение: Используйте псевдонимы (as) или импортируйте только нужные функции.

Ошибка 3: Неправильное имя пакета

// Файл лежит в папке src/main/kotlin/com/example/
package com.example.wrong  // Ошибка! Должно быть com.example

fun test() = "test"

Решение: Имя пакета должно соответствовать структуре папок (хотя технически это не обязательно, но так принято).

Ошибка 4: Циклическая зависимость модулей

// Модуль A зависит от модуля B
// Модуль B зависит от модуля A
// Ошибка сборки!

Решение: Перепроектируйте архитектуру. Вынесите общие части в третий модуль.

Ошибка 5: Импорт несуществующего элемента

import com.example.utils.nonExistentFunction  // Ошибка!

Решение: Проверьте, что функция/класс действительно объявлены в указанном пакете.

Ошибка 6: Использование internal-элементов из другого модуля

// В модуле A:
internal class SecretClass  // Виден только внутри модуля A

// В модуле B:
import com.example.a.SecretClass  // Ошибка! Internal-элементы не видны

Решение: Используйте public для элементов, которые должны быть доступны из других модулей.

Лучшие практики и советы профессионалов

1. Следуйте соглашению об именовании пакетов

Используйте обратный домен: com.example.myapp. Это гарантирует уникальность имён во всём мире.

2. Один файл — одна логическая единица

Не пишите всё в одном файле. Разбивайте код по смыслу: UserService.kt, ProductRepository.kt, MainActivity.kt.

3. Импортируйте конкретно, а не звёздочкой

Вместо import com.example.utils.* пишите import com.example.utils.sayHello. Это делает код понятнее и предотвращает конфликты.

4. Используйте псевдонимы при конфликтах

Если два класса называются одинаково, дайте одному из них псевдоним:

import com.example.old.Date as OldDate
import java.util.Date

5. Структурируйте пакеты по функциональности

Хорошая структура:

com.example.myapp/
├── data/           # Работа с данными
│   ├── model/      # Модели данных
│   ├── repository/ # Репозитории
│   └── database/   # Работа с БД
├── domain/         # Бизнес-логика
│   ├── usecase/    # Сценарии использования
│   └── service/    # Сервисы
├── ui/             # Пользовательский интерфейс
│   ├── screen/     # Экраны
│   └── component/  # Компоненты
└── util/           # Утилиты

6. Избегайте глубокой вложенности пакетов

Не создавайте пакеты глубже 3-4 уровней. Это усложняет навигацию.

7. Используйте модули для разделения ответственности

Если проект большой, разбейте его на модули: app, core, network, database. Каждый модуль должен отвечать за свою область.

8. Документируйте публичные API

Если вы создаёте библиотеку, обязательно документируйте все публичные функции и классы с помощью KDoc:

/**
 * Вычисляет факториал числа.
 * @param n неотрицательное целое число
 * @return факториал числа n
 * @throws IllegalArgumentException если n < 0
 */
fun factorial(n: Int): Long {
    require(n >= 0) { "n должно быть неотрицательным" }
    // ...
}

Вопросы для самопроверки

  1. Вопрос: Что такое пакет в Kotlin и зачем он нужен?

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

  2. Вопрос: Как импортировать все элементы из пакета?

    Ответ: Используйте звёздочку: import com.example.utils.*. Но лучше импортировать только то, что нужно.

  3. Вопрос: Что делать, если два импортированных класса имеют одинаковое имя?

    Ответ: Использовать псевдоним: import com.example.utils.Date as MyDate.

  4. Вопрос: Можно ли в одном файле объявить два пакета?

    Ответ: Нет. В одном файле может быть только одно объявление пакета, и оно должно быть в начале файла.

  5. Вопрос: Чем отличается модуль от пакета?

    Ответ: Модуль — это более крупная единица (библиотека или часть проекта), которая может содержать много пакетов. Пакет — это просто группа файлов внутри модуля.

  6. Вопрос: Что такое extension-функция и как её импортировать?

    Ответ: Extension-функция — это функция, которая добавляет новое поведение существующему классу. Импортируется как обычная функция: import com.example.extensions.isEven.

  7. Вопрос: Какие пакеты импортируются в Kotlin по умолчанию?

    Ответ: kotlin.*, kotlin.annotation.*, kotlin.collections.*, kotlin.comparisons.*, kotlin.io.*, kotlin.ranges.*, kotlin.sequences.*, kotlin.text.*.

  8. Вопрос: Что произойдёт, если создать циклическую зависимость между модулями?

    Ответ: Компилятор выдаст ошибку. Циклические зависимости запрещены в Kotlin.

  9. Вопрос: Как сделать элемент видимым только внутри модуля?

    Ответ: Использовать модификатор internal. Такой элемент будет доступен только внутри того же модуля.

  10. Вопрос: Можно ли импортировать функцию, объявленную в том же файле?

    Ответ: Нет, в этом нет необходимости. Всё, что объявлено в файле, автоматически доступно внутри него.

Итог и что изучить дальше

Мы разобрали одну из важнейших тем в программировании — организацию кода с помощью модулей, пакетов и импортов. Теперь вы знаете:

  • Как объявлять пакеты и импортировать из них элементы.
  • Как избегать конфликтов имён с помощью псевдонимов.
  • Как работают модули и чем они отличаются от пакетов.
  • Какие ошибки чаще всего допускают новички и как их избежать.
  • Лучшие практики организации кода в больших проектах.

Эти знания пригодятся вам в любом проекте — от маленькой консольной утилиты до огромного Android-приложения. Умение правильно организовывать код — это признак профессионального разработчика.

Что изучить дальше:

  1. Модификаторы видимости — public, private, protected, internal. Они тесно связаны с пакетами и модулями.
  2. Gradle и система сборки — как настраивать модули в build.gradle.kts.
  3. Библиотеки и зависимости — как подключать чужие модули (библиотеки) через Maven Central или JitPack.
  4. Архитектура приложений — Clean Architecture, MVVM, как правильно делить проект на модули.
  5. Рефлексия — как получать информацию о классах и пакетах во время выполнения программы.
🎯 Практический совет: Попробуйте взять любой свой старый проект (даже из 2-3 файлов) и переорганизовать его с использованием пакетов. Разбейте код на логические группы, импортируйте только нужные функции. Вы сразу почувствуете разницу в удобстве работы!