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

По умолчанию ваш проект будет использовать Gradle с Kotlin DSL в качестве системы сборки.
Укажите JDK, который необходим для разработки проектов Kotlin.
Нажмите Далее, а затем Готово.
Мастер создаст пример многоплатформенной библиотеки со следующей структурой:
Написание кроссплатформенного кода
Определите классы и интерфейсы, которые вы собираетесь реализовать в общем коде.
В
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-части вашего проекта вы будете использовать пакет npm base-64. Его можно добавить в качестве зависимости для реализации JS-специфичного функционала:
-
Обновите файл
build.gradle.kts, добавив зависимость в наборjsMain:kotlin { // ... sourceSets { val jsMain by getting { dependencies { implementation(npm("base-64", "1.0.0")) } } } } В
jsMain/kotlin, создайте новую директориюorg.jetbrains.base64.Создайте файл
Base64.ktв новой директории.-
Предоставьте простую реализацию объекта
Base64Factory, которая делегирует объектуBase64:package org.jetbrains.base64 actual object Base64Factory { actual fun createEncoder(): Base64Encoder = JsBase64Encoder } object JsBase64Encoder : Base64Encoder { override fun encode(src: ByteArray): ByteArray { val binString = src.decodeToString() return Base64.encode(binString).encodeToByteArray() } } Создайте отдельный файл
Base64Npm.ktв той же директории.-
Реализуйте объект
Base64с использованием пакета npmbase-64:package org.jetbrains.base64 @JsModule("base-64") @JsNonModule external object Base64 { fun encode(s: String): String }
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
Ваша многоплатформенная библиотека готова к публикации, чтобы вы могли использовать её в других проектах.
Для публикации библиотеки используйте maven-publish плагин Gradle.
-
В файле
build.gradle.kts, примените плагинmaven-publishи укажите группу и версию вашей библиотеки:plugins { kotlin("multiplatform") version "1.9.0" id("maven-publish") } group = "org.jetbrains.base64" version = "1.0.0" -
В терминале запустите задачу
publishToMavenLocalGradle, чтобы опубликовать вашу библиотеку в локальный репозиторий Maven:./gradlew publishToMavenLocal
Ваша библиотека будет опубликована в локальный репозиторий Maven.
Опубликуйте вашу библиотеку в внешний репозиторий Maven Central
Вы можете сделать свою многоплатформенную библиотеку общедоступной и опубликовать её в Maven Central, удалённом репозитории, где хранятся и управляются артефакты Maven. Таким образом, другие разработчики смогут её найти и добавить в качестве зависимости в свои проекты.
Регистрация учётной записи Sonatype и генерация ключей GPG
Если это ваша первая библиотека, или вы раньше использовали устаревший Bintray для этой цели, вам сначала необходимо зарегистрировать учётную запись Sonatype.
Вы можете воспользоваться статьёй GetStream, чтобы создать и настроить свою учётную запись. Раздел Регистрация учётной записи Sonatype описывает, как это сделать:
Зарегистрировать учётную запись Jira Sonatype.
Создать новый вопрос. В качестве примера вы можете использовать наш вопрос.
Подтвердить владение доменом, соответствующим идентификатору группы, который вы хотите использовать для публикации своих артефактов.
Поскольку артефакты, опубликованные в Maven Central, должны быть подписаны, следуйте инструкциям в разделе Генерация пары ключей GPG, чтобы:
Сгенерировать пару ключей GPG для подписи ваших артефактов.
Опубликовать ваш открытый ключ.
Экспортировать ваш закрытый ключ.
После того, как репозиторий Maven и ключи подписи для вашей библиотеки будут готовы, вы можете перейти к настройке сборки для загрузки артефактов библиотеки в репозиторий предварительного размещения и затем к публикации.
Настройка публикации
Теперь вам нужно указать Gradle, как опубликовать библиотеку. Большая часть работы уже выполнена плагинами maven-publish и Kotlin Gradle, все необходимые публикации создаются автоматически. Вы уже знаете результат, когда библиотека публикуется в локальный репозиторий Maven. Чтобы опубликовать её в Maven Central, вам нужно выполнить дополнительные шаги:
Настроить URL-адрес публичного репозитория Maven и учетные данные.
Предоставить описание и
javadocsдля всех компонентов библиотеки.Подписать публикации.
Вы можете выполнить все эти задачи с помощью скриптов Gradle. Давайте вынесем всю логику, связанную с публикацией, из модуля библиотеки build.script, чтобы в будущем её было легко повторно использовать для других модулей.
Самый идиоматичный и гибкий способ сделать это — использовать предварительно скомпилированные скриптовые плагины Gradle precompiled script plugins. Вся логика сборки будет предоставлена в виде предварительно скомпилированного скриптового плагина и может применяться по идентификатору плагина ко всем модулям нашей библиотеки.
Для реализации этого перенесите логику публикации в отдельный проект Gradle:
Добавьте новый проект Gradle в корневой проект вашей библиотеки. Для этого создайте новую папку с именем
convention-pluginsи создайте в ней новый файл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для хранения всей логики публикации.-
Добавьте всю необходимую логику в новый файл. Убедитесь, что внесены изменения, соответствующие конфигурации вашего проекта, и там, где явно указано в угловых скобках (например,
<replace-me>):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 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.9.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
Что дальше?
Узнайте больше о опубликовании мультиплатформенных библиотек.
Узнайте больше о Kotlin Multiplatform.
Создание вашего первого кроссплатформенного мобильного приложения – учебник.
© 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