Аналог модификатора доступа package-private в Kotlin
Почему в Kotlin нет прямого аналога package-private
Kotlin сознательно отказался от модификатора package-private в пользу более строгой системы видимости. В Java пакеты — это не только логическая, но и физическая структура (каталоги). В Kotlin пакеты — чисто логическое понятие, файлы могут лежать в разных папках, но принадлежать одному пакету. Поэтому полагаться на файловую структуру для контроля доступа — ненадёжно. Вместо этого Kotlin предлагает internal (видимость в пределах модуля) и более продвинутые техники.
Основное решение: используйте `internal` + модульную структуру
Самый правильный и идиоматичный способ — разбить ваш проект на несколько Gradle-модулей. Например:
- Модуль `:database` — содержит все классы, связанные с БД (ваш аналог пакета).
- Модуль `:app` — основное приложение, которое зависит от `:database`.
Внутри модуля `:database` вы помечаете классы и функции как internal. Они будут видны всем классам внутри этого модуля, но абсолютно недоступны из модуля `:app`.
// Модуль: database
// Файл: DatabaseHelper.kt
package com.example.database
internal class DatabaseHelper { // Виден только внутри модуля ':database'
internal fun connect() {
println("Connecting...")
}
}
// Файл: UserRepository.kt (тоже в модуле ':database')
package com.example.database
class UserRepository {
fun init() {
DatabaseHelper().connect() // Доступно, так как в одном модуле
}
}
// Модуль: app
// Файл: Main.kt
package com.example.app
import com.example.database.UserRepository
fun main() {
UserRepository().init() // Работает
// DatabaseHelper() // Ошибка компиляции! Класс internal и недоступен из другого модуля
}
Этот подход полностью решает вашу задачу: вы получаете изоляцию, даже более строгую, чем package-private (так как модуль обычно больше одного пакета, но в вашем случае модуль может содержать ровно один пакет).
Альтернатива: приватные расширения и соглашения
Если вы не хотите или не можете разбивать проект на модули, можно использовать соглашения о нейминге и приватные функции-расширения. Например, положите все "внутренние" классы в один файл и используйте private:
// Файл: DatabaseInternals.kt
package com.example.database
// Класс с приватным конструктором — доступен только внутри этого файла
class DatabaseHelper private constructor() {
fun connect() { /* ... */ }
companion object {
fun create(): DatabaseHelper = DatabaseHelper()
}
}
// Публичный класс, который использует DatabaseHelper
class UserRepository {
private val helper = DatabaseHelper.create()
fun init() {
helper.connect()
}
}
Минус: все классы, которые должны видеть друг друга, приходится держать в одном файле. Для больших проектов это неудобно.
Самый гибкий вариант: использовать аннотации или sealed классы
Для сложной логики можно использовать sealed class или sealed interface. Они ограничивают иерархию наследования определённым файлом или пакетом (в Kotlin 1.9+ — подпакетами). Это не даёт прямого контроля доступа к методам, но ограничивает создание подклассов, что часто решает ту же задачу.
// Файл: DatabaseOperation.kt
package com.example.database
sealed class DatabaseOperation {
abstract fun execute()
}
class ConnectOperation : DatabaseOperation() {
override fun execute() { /* connect */ }
}
// В другом файле того же пакета:
class UserRepository {
fun performOperation(op: DatabaseOperation) {
op.execute() // Работает, так как ConnectOperation доступен
}
}
Итог: Для 99% случаев используйте internal + модули. Это стандарт в Android-разработке и Kotlin-проектах. Остальные варианты — для специфических ситуаций.
Дополнение: Использование `@PublishedApi` и `@RequiresOptIn`
Если вам нужно, чтобы какой-то внутренний API был доступен только для определённых потребителей (например, для тестов в том же модуле), можно комбинировать internal с аннотациями-маркерами.
Создайте свою аннотацию, помеченную @RequiresOptIn:
// Файл: InternalApi.kt
@RequiresOptIn(
message = "Этот API предназначен только для внутреннего использования в модуле database",
level = RequiresOptIn.Level.ERROR
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class InternalDatabaseApi
Теперь пометьте внутренние классы этой аннотацией:
@InternalDatabaseApi
internal class DatabaseHelper {
fun connect() { /* ... */ }
}
В месте использования (даже внутри того же модуля) компилятор потребует явного разрешения через @OptIn(InternalDatabaseApi::class):
class UserRepository {
@OptIn(InternalDatabaseApi::class)
fun init() {
DatabaseHelper().connect() // Работает только с OptIn
}
}
Этот подход не делает API полностью package-private, но добавляет "соглашение" и предупреждение для разработчиков: "Это внутренняя кухня, используй на свой страх и риск". Полезно для больших команд или библиотек.
Минус: Любой разработчик всё равно может поставить @OptIn и получить доступ. Это не защита на уровне компиляции, а лишь дисциплинарная мера.