Создание и публикация многоплатформенной библиотеки – учебник
В этом учебнике вы узнаете, как создать многоплатформенную библиотеку для JVM, JS и Native платформ, написать общие тесты для всех платформ и опубликовать библиотеку в локальном репозитории Maven.
Эта библиотека преобразует исходные данные – строки и массивы байтов – в формат Base64. Она может использоваться на Kotlin/JVM, Kotlin/JS и любой доступной Kotlin/Native платформе.
Вы будете использовать разные способы реализации преобразования в формат Base64 на разных платформах:
Для JVM – класс
java.util.Base64.Для JS – функция
btoa().Для Kotlin/Native – ваша собственная реализация.
Вы также протестируете свой код с помощью общих тестов и затем опубликуете библиотеку в локальном репозитории Maven.
Настройка среды
Вы можете выполнить этот учебник на любой операционной системе. Скачайте и установите последнюю версию IntelliJ IDEA с последним плагином Kotlin.
Создание проекта
В IntelliJ IDEA выберите Файл | Новый | Проект.
В левой панели выберите Kotlin Multiplatform.
-
Введите имя проекта, а затем в разделе Multiplatform выберите Библиотека в качестве шаблона проекта.

По умолчанию ваш проект будет использовать Gradle с Kotlin DSL в качестве системы сборки.
Укажите JDK, который необходим для разработки проектов Kotlin.
Нажмите Далее, а затем Готово.
- Дополнительная конфигурация проекта
-
Для более сложных проектов вам может потребоваться добавить больше модулей и целей:
Для добавления модулей выберите Проект и нажмите значок +. Выберите тип модуля.
Для добавления целевых платформ выберите библиотеку и нажмите значок +. Выберите целевую платформу.
-
Настройте параметры целевой платформы, такие как версия целевой JVM и фреймворк для тестов.

-
Если необходимо, укажите зависимости между модулями:
Многоплатформенные и Android модули
Многоплатформенные и iOS модули
JVM модули

Мастер создаст пример многоплатформенной библиотеки со следующей структурой:

Написание кроссплатформенного кода
Определите классы и интерфейсы, которые вы собираетесь реализовать в общем коде.
В каталоге
commonMain/kotlin, создайте пакетorg.jetbrains.base64.Создайте файл
Base64.ktв новом пакете.-
Определите интерфейс
Base64Encoder, который преобразует байты в форматBase64:package org.jetbrains.base64 interface Base64Encoder { fun encode(src: ByteArray): ByteArray } -
Определите объект
Base64Factory, чтобы предоставить экземпляр интерфейсаBase64Encoderобщему коду:expect object Base64Factory { fun createEncoder(): Base64Encoder }
Объект-фабрика помечен ключевым словом expect в кроссплатформенном коде. Для каждой платформы вы должны предоставить реализацию actual объекта Base64Factory с платформенно-специфическим кодером. Подробнее о платформенно-специфических реализациях.
Предоставление платформенно-специфических реализаций
Теперь вы создадите actual реализации объекта Base64Factory для каждой платформы:
JVM
В каталоге
jvmMain/kotlin, создайте пакетorg.jetbrains.base64.Создайте файл
Base64.ktв новом пакете.-
Предоставьте простую реализацию объекта
Base64Factory, которая делегирует классуjava.util.Base64:package org.jetbrains.base64 import java.util.* actual object Base64Factory { actual fun createEncoder(): Base64Encoder = JvmBase64Encoder } object JvmBase64Encoder : Base64Encoder { override fun encode(src: ByteArray): ByteArray = Base64.getEncoder().encode(src) }
Довольно просто, не так ли? Вы предоставили платформенно-специфическую реализацию, используя прямое делегирование сторонней реализации.
JS
Реализация JS будет очень похожа на JVM.
В каталоге
jsMain/kotlin, создайте пакетorg.jetbrains.base64.Создайте файл
Base64.ktв новом пакете.-
Предоставьте простую реализацию объекта
Base64Factory, которая делегирует функцииbtoa().package org.jetbrains.base64 import kotlinx.browser.window actual object Base64Factory { actual fun createEncoder(): Base64Encoder = JsBase64Encoder } object JsBase64Encoder : Base64Encoder { override fun encode(src: ByteArray): ByteArray { val string = src.decodeToString() val encodedString = window.btoa(string) return encodedString.encodeToByteArray() } }
Native
К сожалению, для всех целей Kotlin/Native нет доступной сторонней реализации, поэтому вам нужно написать ее самостоятельно.
В каталоге
nativeMain/kotlin, создайте пакетorg.jetbrains.base64.Создайте файл
Base64.ktв новом пакете.-
Предоставьте собственную реализацию объекта
Base64Factory:package org.jetbrains.base64 private val BASE64_ALPHABET: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" private val BASE64_MASK: Byte = 0x3f private val BASE64_PAD: Char = '=' private val BASE64_INVERSE_ALPHABET = IntArray(256) { BASE64_ALPHABET.indexOf(it.toChar()) } private fun Int.toBase64(): Char = BASE64_ALPHABET[this] actual object Base64Factory { actual fun createEncoder(): Base64Encoder = NativeBase64Encoder } object NativeBase64Encoder : Base64Encoder { override fun encode(src: ByteArray): ByteArray { fun ByteArray.getOrZero(index: Int): Int = if (index >= size) 0 else get(index).toInt() // 4n / 3 is expected Base64 payload val result = ArrayList<Byte>(4 * src.size / 3) var index = 0 while (index < src.size) { val symbolsLeft = src.size - index val padSize = if (symbolsLeft >= 3) 0 else (3 - symbolsLeft) * 8 / 6 val chunk = (src.getOrZero(index) shl 16) or (src.getOrZero(index + 1) shl 8) or src.getOrZero(index + 2) index += 3 for (i in 3 downTo padSize) { val char = (chunk shr (6 * i)) and BASE64_MASK.toInt() result.add(char.toBase64().code.toByte()) } // Fill the pad with '=' repeat(padSize) { result.add(BASE64_PAD.code.toByte()) } } return result.toByteArray() } }
Тестирование вашей библиотеки
Теперь, когда у вас есть actual реализации объекта Base64Factory для всех платформ, пришло время протестировать вашу многоплатформенную библиотеку.
Чтобы сэкономить время на тестировании, вы можете написать общие тесты, которые будут выполняться на всех платформах вместо тестирования каждой платформы по отдельности.
Предварительные условия
Перед написанием тестов добавьте метод encodeToString с реализацией по умолчанию в интерфейс Base64Encoder, который определен в commonMain/kotlin/org/jetbrains/base64/Base64.kt. Эта реализация преобразует массивы байтов в строки, что значительно упрощает тестирование.
interface Base64Encoder {
fun encode(src: ByteArray): ByteArray
fun encodeToString(src: ByteArray): String {
val encoded = encode(src)
return buildString(encoded.size) {
encoded.forEach { append(it.toInt().toChar()) }
}
}
}
Вы также можете предоставить более эффективную реализацию этого метода для конкретной платформы, например, для JVM в jvmMain/kotlin/org/jetbrains/base64/Base64.kt:
object JvmBase64Encoder : Base64Encoder {
override fun encode(src: ByteArray): ByteArray = Base64.getEncoder().encode(src)
override fun encodeToString(src: ByteArray): String = Base64.getEncoder().encodeToString(src)
}
Одним из преимуществ многоплатформенной библиотеки является наличие реализации по умолчанию с опциональными платформно-специфическими переопределениями.
Написание общих тестов
Теперь у вас есть API, основанный на строках, который можно покрыть базовыми тестами.
В каталоге
commonTest/kotlinсоздайте пакетorg.jetbrains.base64.Создайте файл
Base64Test.ktв новом пакете.-
Добавьте тесты в этот файл:
package org.jetbrains.base64 import kotlin.test.Test import kotlin.test.assertEquals class Base64Test { @Test fun testEncodeToString() { checkEncodeToString("Kotlin is awesome", "S290bGluIGlzIGF3ZXNvbWU=") } @Test fun testPaddedStrings() { checkEncodeToString("", "") checkEncodeToString("1", "MQ==") checkEncodeToString("22", "MjI=") checkEncodeToString("333", "MzMz") checkEncodeToString("4444", "NDQ0NA==") } private fun checkEncodeToString(input: String, expectedOutput: String) { assertEquals(expectedOutput, Base64Factory.createEncoder().encodeToString(input.asciiToByteArray())) } private fun String.asciiToByteArray() = ByteArray(length) { get(it).code.toByte() } } -
В Терминале выполните задачу Gradle
check:./gradlew check
Тесты будут выполняться на всех платформах (JVM, JS и Native).
Добавление платформно-специфических тестов
Вы также можете добавить тесты, которые будут выполняться только для определенной платформы. Например, вы можете добавить тесты UTF-16 на JVM:
В каталоге
jvmTest/kotlinсоздайте пакетorg.jetbrains.base64.Создайте файл
Base64Test.ktв новом пакете.-
Добавьте тесты в этот файл:
package org.jetbrains.base64 import kotlin.test.Test import kotlin.test.assertEquals class Base64JvmTest { @Test fun testNonAsciiString() { val utf8String = "Gödel" val actual = Base64Factory.createEncoder().encodeToString(utf8String.toByteArray()) assertEquals("R8O2ZGVs", actual) } }
Этот тест будет автоматически выполняться на платформе JVM помимо общих тестов.
Опубликовать вашу библиотеку в локальный репозиторий Maven
Ваша многоплатформенная библиотека готова к публикации, чтобы вы могли использовать её в других проектах.
Для публикации вашей библиотеки используйте плагин Gradle maven-publish.
-
В файле
build.gradle.ktsпримените плагинmaven-publishи укажите группу и версию вашей библиотеки:plugins { kotlin("multiplatform") version "1.8.0" id("maven-publish") } group = "org.jetbrains.base64" version = "1.0.0" -
В Терминале выполните задачу Gradle
publishToMavenLocalдля публикации вашей библиотеки в локальный репозиторий Maven:./gradlew publishToMavenLocal
Ваша библиотека будет опубликована в локальном репозитории Maven.
Опубликуйте свою библиотеку в внешний репозиторий Maven Central
Вы можете опубликовать свою многоплатформенную библиотеку в Maven Central, удаленный репозиторий, где хранятся и управляются артефакты Maven. Таким образом, другие разработчики смогут найти её и добавить в качестве зависимости в свои проекты.
Зарегистрировать учётную запись Sonatype и сгенерировать ключи GPG
Если это ваша первая библиотека или вы раньше использовали устаревший Bintray, вам сначала нужно зарегистрировать учётную запись Sonatype.
Вы можете воспользоваться статьёй GetStream для создания и настройки своей учётной записи. В разделе Регистрация учётной записи Sonatype описано, как:
Зарегистрировать учётную запись Sonatype Jira.
Создать новую задачу. Вы можете использовать нашу задачу в качестве примера.
Подтвердить владение доменом, соответствующим идентификатору группы, который вы хотите использовать для публикации артефактов.
Затем, поскольку артефакты, опубликованные в Maven Central, должны быть подписаны, выполните действия из раздела Генерация пары ключей GPG, чтобы:
Сгенерировать пару ключей GPG для подписи артефактов.
Опубликовать ваш открытый ключ.
Экспортировать ваш закрытый ключ.
После подготовки репозитория Maven и ключей подписи для вашей библиотеки, вы можете перейти к настройке сборки для загрузки артефактов библиотеки в репозиторий подготовки, а затем опубликовать их.
Настройка публикации
Теперь вам нужно указать Gradle, как опубликовать библиотеку. Большая часть работы уже выполнена плагинами Gradle для maven-publish и Kotlin, все необходимые публикации создаются автоматически. Вы уже знаете результат, когда библиотека опубликована в локальный репозиторий Maven. Чтобы опубликовать её в Maven Central, вам необходимо выполнить дополнительные шаги:
Настройте URL-адрес публичного репозитория Maven и учетные данные.
Укажите описание и
javadocsдля всех компонентов библиотеки.Подпишите публикации.
Вы можете выполнить все эти задачи с помощью скриптов Gradle. Давайте вынесем всю логику, связанную с публикацией, из модуля библиотеки build.script, чтобы в будущем легко её повторно использовать для других модулей.
Самый удобный и гибкий способ сделать это — использовать предварительно скомпилированные скриптовые плагины Gradle прекомпилированные скриптовые плагины. Вся логика сборки будет предоставляться в виде предварительно скомпилированного скриптового плагина и может применяться по идентификатору плагина ко всем модулям нашей библиотеки.
Для этого перенесите логику публикации в отдельный проект Gradle:
Добавьте новый проект Gradle в корневой проект вашей библиотеки. Для этого создайте новую папку с именем
convention-pluginsс файломsrc/build.gradle.ktsв ней.-
Обновите этот файл
build.gradle.ktsследующим кодом:plugins { "kotlin-dsl" // Is needed to turn our build logic written in Kotlin into the Gradle Plugin } repositories { gradlePluginPortal() // To use 'maven-publish' and 'signing' plugins in our own plugin } В директории
convention-plugins/srcсоздайте файлmain/kotlin/convention.publication.gradle.ktsдля хранения всей логики публикации.-
Добавьте всю необходимую логику в новый файл:
import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.bundling.Jar import org.gradle.kotlin.dsl.`maven-publish` import org.gradle.kotlin.dsl.signing import java.util.* plugins { 'maven-publish' signing } // Заглушка для секретов, чтобы проект синхронизировался и собирался без значений публикации ext["signing.keyId"] = null ext["signing.password"] = null ext["signing.secretKeyRingFile"] = null ext["ossrhUsername"] = null ext["ossrhPassword"] = null // Получение секретов из файла local.properties или из переменных окружения, которые могут использоваться в CI val secretPropsFile = project.rootProject.file("local.properties") if (secretPropsFile.exists()) { secretPropsFile.reader().use { Properties().apply { load(it) } }.onEach { (name, value) -> ext[name.toString()] = value } } else { ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID") ext["signing.password"] = System.getenv("SIGNING_PASSWORD") ext["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE") ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME") ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD") } val javadocJar by tasks.registering(Jar::class) { archiveClassifier.set("javadoc") } fun getExtraString(name: String) = ext[name]?.toString() publishing { // Настройка репозитория maven central repositories { maven { name = "sonatype" setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") credentials { username = getExtraString("ossrhUsername") password = getExtraString("ossrhPassword") } } } // Настройка всех публикаций publications.withType<MavenPublication> { // Заглушка для артефакта javadoc.jar artifact(javadocJar.get()) // Предоставление информации об артефактах, необходимой Maven Central pom { name.set("MPP Sample library") description.set("Sample Kotlin Multiplatform library (jvm + ios + js) test") url.set("https://github.com/<your-github-repo>/mpp-sample-lib") licenses { license { name.set("MIT") url.set("https://opensource.org/licenses/MIT") } } developers { developer { id.set("<your-github-profile>") name.set("<your-name>") email.set("<your-email>") } } scm { url.set("https://github.com/<your-github-repo>/mpp-sample-lib") } } } } // Подпись артефактов. Значения свойств Signing.* будут использоваться signing { sign(publishing.publications) }Применения только
maven-publishдостаточно для публикации в локальном репозитории Maven, но не в Maven Central. В предоставленном скрипте вы получаете учетные данные изlocal.propertiesили переменных окружения, выполняете всю необходимую настройку в разделеpublishing, и подписываете свои публикации с помощью плагина подписи. -
Вернитесь к своему проекту библиотеки. Чтобы попросить Gradle предварительно собрать ваши плагины, обновите корневой файл
settings.gradle.ktsследующим образом:rootProject.name = "multiplatform-lib" // your project name includeBuild("convention-plugins") -
Теперь вы можете применить эту логику в модуле
build.scriptбиблиотеки. В разделеpluginsзаменитеmaven-publishнаconventional.publication.plugins { kotlin("multiplatform") version "1.8.0" id("convention.publication") } -
Создайте файл
local.propertiesсо всеми необходимыми учетными данными и убедитесь, что добавили его в.gitignore.# The GPG key pair ID (last 8 digits of its fingerprint) signing.keyId=... # The passphrase of the key pair signing.password=... # Private key you exported earlier signing.secretKeyRingFile=... # Your credentials for the Jira account ossrhUsername=... ossrhPassword=...
Запустите
./gradlew cleanи синхронизируйте проект.
Новые задачи Gradle, связанные с репозиторием Sonatype, должны появиться в группе публикации, это означает, что всё готово для публикации вашей библиотеки.
Опубликуйте свою библиотеку в Maven Central
Чтобы загрузить свою библиотеку в репозиторий Sonatype, выполните следующую команду:
./gradlew publishAllPublicationsToSonatypeRepository
Репозиторий подготовки будет создан, и все артефакты всех публикаций будут загружены в этот репозиторий. Осталось проверить, что все необходимые артефакты были загружены, и нажать кнопку «Опубликовать».
Эти шаги описаны в разделе Ваша первая публикация. Вкратце, вам нужно:
Перейдите на https://s01.oss.sonatype.org и войдите в систему, используя ваши учетные данные в Sonatype Jira.
Найдите свой репозиторий в разделе Репозитории подготовки.
Закройте его.
Опубликуйте библиотеку.
Для активации синхронизации с Maven Central вернитесь к созданной задаче Jira и оставьте комментарий, что вы опубликовали свой первый компонент. Этот шаг необходим только при первой публикации.
Вскоре ваша библиотека станет доступна по адресу https://repo1.maven.org/maven2, и другие разработчики смогут добавить её в качестве зависимости. Через пару часов другие разработчики смогут найти её с помощью Поиска репозитория Maven Central.
Добавление зависимости на опубликованную библиотеку
Вы можете добавить свою библиотеку в другие проекты многоплатформенной разработки в качестве зависимости.
В файле build.gradle.kts добавьте mavenLocal() или MavenCentral() (если библиотека была опубликована в внешний репозиторий) и добавьте зависимость от вашей библиотеки:
repositories {
mavenCentral()
mavenLocal()
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.base64:multiplatform-lib:1.0.0")
}
}
}
}
Зависимость implementation состоит из:
Идентификатор группы и версия — указанные ранее в файле
build.gradle.ktsИдентификатор артефакта — по умолчанию, это имя вашего проекта, указанное в файле
settings.gradle.kts
Для получения более подробной информации см. документацию Gradle по плагину maven-publish
Что дальше?
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform-library.html