Общий и платформенный код в одном модуле

Граница между общим и платформенным кодом — и как решить, по какую сторону положить класс.

Платформенно-нейтральный код — код, который зависит только от стандартной библиотеки Kotlin и мультиплатформенных зависимостей, без обращений к API конкретной ОС.

Где проходит граница

Решая, делить класс или нет, спросите: «Зависит ли он от платформы?» Если класс считает скидку, валидирует email, парсит JSON — он нейтрален, ему место в commonMain. Если он открывает файл, читает SharedPreferences, показывает уведомление — он платформенный.

Серая зона — вещи вроде текущего времени, генерации UUID, логирования. Они концептуально общие, но реализуются через платформенный API. Их выносят в общий код как интерфейс или expect-объявление, а реализацию дают на каждой платформе.

Пример разделения

Бизнес-логика — общая:

// commonMain
class PriceCalculator {
    fun finalPrice(base: Double, discountPercent: Int): Double {
        require(discountPercent in 0..100)
        return base * (100 - discountPercent) / 100.0
    }
}

А вот доступ к хранилищу — платформенный, и в общий код он попадает через абстракцию:

// commonMain
interface KeyValueStore {
    fun putString(key: String, value: String)
    fun getString(key: String): String?
}

На Android реализация обернёт SharedPreferences, на iOS — NSUserDefaults. Общий код работает только с интерфейсом и не знает, что под ним.

Как работает под капотом

Этот приём — обычная инверсия зависимостей. Общий код объявляет контракт (interface) и принимает его реализацию извне (через конструктор или DI). Компилятор общего набора видит только интерфейс и спокойно собирается под все цели. Конкретные классы живут в платформенных наборах и «доезжают» до общего кода во время выполнения. Это масштабируемая альтернатива expect/actual: для сложных зависимостей интерфейс гибче, для одиночных функций и констант — лаконичнее expect/actual.

Частые ошибки

Соблазн — добавить в общий интерфейс метод, который имеет смысл только на одной платформе («показать Android-тост»). Так протекает платформенность. Интерфейсы в общем коде должны описывать возможности, а не платформенные детали: не «showAndroidToast», а «showMessage». Вторая ошибка — выносить в общий код слишком мало, оставляя дублирование; начните с самого ценного — сетевого и доменного слоя.

Итоги

  • Нейтральный код — в commonMain; платформенный — в платформенных наборах.
  • Серую зону выносят через интерфейс или expect/actual.
  • Общий код зависит от абстракций, реализации приходят с платформ.
  • Интерфейсы описывают возможности, а не платформенные детали.
Проверьте себя
1. Куда положить класс PriceCalculator, считающий скидку?
AВ androidMain
BВ iosMain
CВ commonMain — он платформенно-нейтрален
DВ тесты
2. Как общий код получает доступ к платформенному хранилищу?
AНапрямую вызывает SharedPreferences
BЧерез интерфейс/expect, реализация которого приходит с платформы
CЧерез рефлексию
DНикак, это невозможно