Мультиплатформенные проекты

Мультиплатформенные проекты - это новая экспериментальная возможность Kotlin 1.2. Все описываемые здесь возможности языка могут претерпеть изменения в последующих версиях Kotlin.

Мультиплатформенные проекты Kotlin позволяют вам компилировать один и тот же код для многих целевых платформ. На данный момент поддерживаемыми целевыми платформами являются JVM и JS, с добавлением Native в перспективе.

Структура мультиплатформенного проекта

Мультиплатформенный проект состоит из трех типов модулей:

  • Общий модуль содержит код, который не свойствен какой-либо определённой платформе, а также обьявления для реализации в платформо-зависимых API. Эти обьявления позволяют общему коду быть зависимостью для реализаций для конкретных платформ.
  • Платформенный модуль содержит реализации платформо-зависимых обьявлений из общего модуля для конкретной платформы и другой платформенный код. Платформенный модуль всегда является реализацией одного общего модуля.
  • Обычный модуль. Такие модули базируются на определённой платформе и могут либо быть зависимостью платформенных модулей, либо зависеть от них.

Общий модуль может зависеть только от других общих модулей и библиотек, включая общую версию стандартной библиотеки Kotlin (kotlin-stdlib-common). Общие модули содержат только код на Kotlin и ни на каких иных языках.

Платформенный модуль можеть зависеть от любых модулей и библиотек для заданной платформы (включая библиотеки Java в случае с Kotlin/JVM и библиотеки JavaScript для Kotlin/JS). Платформенные модули для Kotlin/JVM также могут содержать код на Java и других языках для JVM.

Результат компиляции общего модуля - специальный файл метаданных, содержащий все обьявления в этом модуле. Результат компиляции платформенного модуля - код для заданной платформы (байт-код JVM или исходный код JS) для исходного кода Kotlin как в платформенном, так и в реализуемом общем модуле.

Следовательно, каждую мультиплатформенную библиотеку необходимо распространять как набор артефактов - .jar с метаданными для общего кода и несколько .jar для каждого платформенного модуля.

Подготовка мультиплатформенного проекта

Kotlin 1.2 поддерживает сборку мультиплатформенных проектов только с Gradle; другие системы сборки не поддерживаются.

Для создания нового мультиплатформенного проекта в IDE откройте окно создания проекта и выберите опцию "Kotlin (Multiplatform)" во вкладке "Kotlin". IDE создаст проект с тремя модулями: общий и два платформенных для JVM и JS. Для добавления дополнительных модулей откройте окно создания модуля и выберите одну из опций "Kotlin (Multiplatform)" во вкладке "Gradle".

Если вам необходимо настроить проект вручную:

  1. Добавьте плагин Kotlin для Gradle в classpath скрипта сборки: classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  2. Примените плагин kotlin-platform-common к общему модулю
  3. Добавьте зависимость kotlin-stdlib-common к общему модулю
  4. Примените плагины kotlin-platform-jvm и kotlin-platform-js для соответствующих платформенных модулей
  5. Добавьте зависимости с областью expectedBy от платформенных модулей к общему

Пример файла build.gradle для общего модуля с Kotlin 1.2-Beta:

buildscript {
    ext.kotlin_version = '{{ site.data.releases.latest.version }}'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin-platform-common'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version"
}

Пример файла build.gradle для платформенного модуля JVM. Обратите внимание на expectedBy в блоке dependencies:

buildscript {
    ext.kotlin_version = '{{ site.data.releases.latest.version }}'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin-platform-jvm'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    expectedBy project(":")
    testCompile "junit:junit:4.12"
    testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}

Платформо-зависимые обьявления

Одна из ключевых возможностей мультиплатформенного кода Kotlin - способ организации зависимости общего кода от платформенного. В других языках используется методика создания набора интерфейсов в общем коде и их реализация в платформенном. Однако, этот подход не идеален в тех случаях, когда у вас есть библиотека для целевой платформы, имеющая нужную вам функциональность, и вы хотите использовать API этой библиотеки напрямую, без дополнительных обёрток. Также, вы будете вынуждены представлять все общие обьявления в виде интерфейсов, что не всегда применимо.

Kotlin же имеет механизм ожидаемых и актуальных обьявлений. С этим механизмом общий модуль может иметь ожидаемые обьявления, а платформенный модуль - актуальные обьявления, соответствующие ожидаемым. Чтобы понять, как это работает, взглянем на пример. Отрывок кода общего модуля:

package org.jetbrains.foo

expect class Foo(bar: String) {
    fun frob()
}

fun main(args: Array<String>) {
    Foo("Hello").frob()
}

Код соответствующего платформенного JVM-модуля:

package org.jetbrains.foo

actual class Foo actual constructor(val bar: String) {
    actual fun frob() {
        println("Frobbing the $bar")
    }
}

Пример описывает несколько важных моментов:

  • Ожидаемые обьявления в общих модулях и соответствующие им актуальные обьявления всегда имеют абсолютно одинаковые имена.
  • Ожидаемые обьявления помечены ключевым словом expect, а актуальные - actual.
  • Все актуальные обьявления, соответствующие любому ожидаемому обьявлению, должны быть помечены ключевым словом actual.
  • Ожидаемые обьявления никогда не реализуются в общем модуле.

Заметьте, что не только интерфейсы и их члены могут быть помечены как ожидаемые. В этом примере ожидаемый класс имеет конструктор, и его обьекты могут быть созданы прямо из общего кода. Вы также можете применять модификатор expect к обьявлениям на верхнем уровне и аннотациям:

// Общий код
expect fun formatString(source: String, vararg args: Any): String

expect annotation class Test

// JVM
actual fun formatString(source: String, vararg args: Any) =
    String.format(source, args)

actual typealias Test = org.junit.Test

Компилятор следит за тем, чтобы каждое ожидаемое обьявление имело соответствующее актуальное обьявление во всех платформенных модулях, реализующих соответствующий общий модуль, и сообщает об ошибке если какие-либо актуальные обьявления отсутствуют. IDE имеет средства, которые могут помочь создать отсутствующие ожидаемые обьявления.

Если у вас есть платформенная библиотека, которую вы хотите использовать в общем коде и одновременно иметь собственную реализацию для другой платформы, вы можете создать псевдоним для существующего класса как актуальное обьявление:

expect class AtomicRef<V>(value: V) {
    fun get(): V
    fun set(value: V)
    fun getAndSet(value: V): V
    fun compareAndSet(expect: V, update: V): Boolean
}

actual typealias AtomicRef<V> = java.util.concurrent.atomic.AtomicReference<V>

Мультиплатформенные тесты

Вы можете иметь тесты в общем проекте, которые будут скомпилированы и запущены в каждом платформенном проекте. Для этого в пакете kotlin.test есть 4 аннотации дя пометки тестов в общем коде: @Test, @Ignore, @BeforeTest и @AfterTest. На платформе JVM эти аннотации соответствуют аннотациям JUnit 4, а в модуле для JS они уже доступны с версии Kotlin 1.1.4 для поддержки модульного тестирования кода JS.

Для их использования добавьте зависимость kotlin-test-annotations-common к общему модулю, kotlin-test-junit к платформенному модулю JVM и kotlin-test-js к модулю для JS.