Многоплатформенная библиотека Kotlin
| Дата последнего обновления | 31 июля 2020 |
Вы узнаете пошагово, как создать многоплатформенную библиотеку, которую можно использовать из любого другого общего кода (например, совместно с Android и iOS). Вы также узнаете, как писать тесты, которые будут выполняться на всех платформах, и использовать эффективную реализацию, предоставляемую конкретной платформой.
Что мы создаём?
Наша цель — создать небольшую многоплатформенную библиотеку, чтобы продемонстрировать возможность совместного использования кода между платформами и её преимущества. Чтобы иметь небольшую реализацию, сфокусированную на многоплатформенной машине, мы напишем библиотеку, которая преобразует необработанные данные (строки и массивы байтов) в формат Base64, который можно использовать на JVM, JS и любой доступной платформе Kotlin/Native.
В реализации для JVM будет использоваться java.util.Base64, которая известна своей высокой эффективностью, потому что JVM знает об этом классе и компилирует его особым образом.
В JS мы будем использовать родной API Buffer, а в Kotlin/Native — собственную реализацию.
Мы покроем эту функциональность общими тестами и затем опубликуем окончательную библиотеку в Maven.
Настройка локальной среды
В этом учебнике мы будем использовать IntelliJ IDEA Community Edition, хотя можно использовать и Ultimate Edition.
Установите плагин Kotlin 1.4.x или выше в IDE. Вы можете проверить версию Kotlin в Tools | Kotlin | Настроить обновления плагина.
Native часть этого проекта написана с использованием Mac OS X, но не беспокойтесь, если вы используете другую платформу, платформа влияет только на имена каталогов в этом конкретном учебнике.
Создание проекта
- В IntelliJ IDEA выберите File | New | Project.
- В панели слева выберите Kotlin.
-
Введите имя проекта и выберите Библиотека под Многоплатформенная в качестве шаблона проекта.
- Нажмите Next, а на следующем экране нажмите Finish.
Теперь создана и импортирована в IntelliJ IDEA многоплатформенная библиотека-пример.
Перейдите к любому файлу .kt и переименуйте пакет с помощью действия IntelliJ IDEA Рефакторинг | Переименовать на org.jetbrains.base64
Давайте проверим, всё ли в порядке с проектом, структура проекта должна быть:
└── src
├── commonMain
│ └── kotlin
├── commonTest
│ └── kotlin
├── jsMain
│ └── kotlin
├── jsTest
│ └── kotlin
├── jvmMain
│ └── kotlin
├── jvmTest
│ └── kotlin
├── macosMain
│ └── kotlin
└── macosTest
└── kotlin
И папка kotlin должна содержать подпапку org.jetbrains.base64
Общий код
Теперь нам нужно определить классы и интерфейсы, которые мы хотим реализовать. Создайте файл Base64.kt в папке commonMain/kotlin/jetbrains/base64
Основной примитив будет интерфейсом Base64Encoder, который знает, как преобразовывать байты в байты в формате Base64
interface Base64Encoder {
fun encode(src: ByteArray): ByteArray
}
Но общий код должен как-то получить экземпляр этого интерфейса, для этого мы определяем фабричный объект Base64Factory
expect object Base64Factory {
fun createEncoder(): Base64Encoder
}
Наш фабричный объект помечен ключевым словом expect. expect — это механизм определения требования, которое каждая платформа должна предоставить, чтобы общая часть работала должным образом. Итак, на каждой платформе мы должны предоставить actual Base64Factory, который знает, как создать кодировщик, специфичный для платформы. Подробнее об платформенно-специфических объявлениях.
Реализации, специфичные для платформы
Теперь настало время предоставить реализацию actual для Base64Factory для каждой платформы.
JVM
Мы начинаем с реализации для JVM. Давайте создадим файл Base64.kt в папке jvmMain/kotlin/jetbrains/base64 и предоставим простую реализацию, которая делегирует java.util.Base64
actual object Base64Factory {
actual fun createEncoder(): Base64Encoder = JvmBase64Encoder
}
object JvmBase64Encoder : Base64Encoder {
override fun encode(src: ByteArray): ByteArray = Base64.getEncoder().encode(src)
}
Довольно просто, не так ли? Мы предоставили платформенно-специфическую реализацию, но использовали прямую делегацию реализации, написанной кем-то другим!
JS
Наша реализация JS будет очень похожа на JVM. Мы создаём файл Base64.kt в папке jsMain/kotlin/jetbrains/base64 и предоставляем реализацию, которая делегирует NodeJS API Buffer
actual object Base64Factory {
actual fun createEncoder(): Base64Encoder = JsBase64Encoder
}
object JsBase64Encoder : Base64Encoder {
override fun encode(src: ByteArray): ByteArray {
val buffer = js("Buffer").from(src)
val string = buffer.toString("base64") as String
return ByteArray(string.length) { string[it].toByte() }
}
}
Native
На общей платформе Native у нас нет возможности использовать реализацию кого-то другого, поэтому нам придётся написать её самим. Это довольно просто и соответствует описанию формата 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().toByte())
}
// Fill the pad with '='
repeat(padSize) { result.add(BASE64_PAD.toByte()) }
}
return result.toByteArray()
}
}
Теперь у нас есть реализации на всех платформах, и пришло время протестировать нашу библиотеку.
Тестирование
Чтобы сделать библиотеку полной, мы должны написать некоторые тесты, но у нас есть три независимые реализации, и это трата времени — писать дубликаты тестов для каждой.
Хорошая вещь в общем коде заключается в том, что его можно покрыть общими тестами, которые затем компилируются и выполняются на каждой платформе.
Давайте создадим класс Base64Test в папке commonTest/kotlin/jetbrains/base64 и напишем базовые тесты для Base64.
Но, как вы помните, наш API преобразует массивы байтов в массивы байтов в другом формате, и протестировать массивы байтов не так просто. Поэтому, прежде чем начать писать тест, добавим метод encodeToString с реализацией по умолчанию в наш интерфейс Base64Encoder
interface Base64Encoder {
fun encode(src: ByteArray): ByteArray
fun encodeToString(src: ByteArray): String {
val encoded = encode(src)
return buildString(encoded.size) {
encoded.forEach { append(it.toChar()) }
}
}
}
Обратите внимание, что реализация на каждой платформе может кодировать массивы байтов в строку. Если мы хотим, мы можем предоставить более эффективную реализацию для этого метода, например, специализируем его на JVM:
object JvmBase64Encoder : Base64Encoder {
override fun encode(src: ByteArray): ByteArray = Base64.getEncoder().encode(src)
override fun encodeToString(src: ByteArray): String = Base64.getEncoder().encodeToString(src)
}
Реализации по умолчанию с необязательными более специализированными переопределениями — ещё одно преимущество многоплатформенной библиотеки. Теперь, когда у нас есть API, работающий со строками, мы можем покрыть его базовыми тестами:
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).toByte()
}
}
Используйте Gradle 6.0 и выше для генерации оболочки (gradle wrapper) в корне проекта, чтобы сгенерировать gradlew, gradlew.bat, и gradle/wrapper/gradle-wrapper.jar
Выполните ./gradlew check, и вы увидите, что тесты выполняются три раза: на JVM, JS и Native!
Если мы хотим, мы можем добавить тесты на определённую платформу, тогда они будут выполняться только как часть тестов этой платформы.
Например, мы можем добавить тесты UTF-16 на JVM. Просто следуйте тем же шагам, но создайте файл в jvmTest/kotlin/jetbrains/base64
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
apply plugin: 'maven-publish' group 'org.jetbrains.base64' version '1.0.0'
Теперь проверьте это командой ./gradlew publishToMavenLocal, и вы должны увидеть успешную сборку.
Всё, наша библиотека теперь успешно опубликована, и любой проект Kotlin может зависеть от неё, будь то другая общая библиотека, JVM, JS или Native приложение.
В этом учебнике мы:
- Создали многоплатформенную библиотеку со специфичными для платформы реализациями.
- Предоставили реализацию по умолчанию для общей части и специализировали её на JVM.
- Написали общие тесты, которые выполняются на каждой платформе.
- Опубликовали окончательную библиотеку в репозитории Maven.
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/tutorials/mpp/multiplatform-library.html