Spec-Zone.ru › Kotlin 1.7

Подключение к платформам-специфичным API

Функция expect/actual находится в стадии Бета. Она почти стабильна, но в будущем могут потребоваться шаги по миграции. Мы сделаем все возможное, чтобы минимизировать изменения, которые вам придется внести.

Если вы разрабатываете многоплатформенное приложение, которому необходимо получить доступ к платформам-специфичным API, которые реализуют необходимую функциональность (например, генерации UUID), используйте механизм Kotlin для ожидаемых и фактических объявлений.

С помощью этого механизма, общий набор исходных файлов определяет ожидаемое объявление, а наборы исходных файлов платформ должны предоставить фактическое объявление, соответствующее ожидаемому объявлению. Это работает для большинства объявлений Kotlin, таких как функции, классы, интерфейсы, перечисления, свойства и аннотации.

Expect/actual declarations in common and platform-specific modules

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

Используйте ожидаемые и фактические объявления только для объявлений Kotlin, которые имеют платформенно-специфические зависимости. Реализация большей части функциональности в общем модуле предпочтительнее, даже если это займет больше времени.

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

Узнайте, как добавить зависимости от платформа-специфичных библиотек.

Примеры

Для простоты в следующих примерах используются интуитивные имена целей, такие как iOS и Android. Однако в ваших файлах Gradle вам необходимо использовать конкретное имя цели из списка поддерживаемых целей.

Генерация UUID

Предположим, что вы разрабатываете приложения iOS и Android с помощью Kotlin Multiplatform Mobile и хотите сгенерировать универсальный уникальный идентификатор (UUID):

Expect/actual declarations for getting the UUID

Для этой цели объявите ожидаемую функцию randomUUID() с ключевым словом expect в общем модуле. Не включайте никакой код реализации.

// Common
expect fun randomUUID(): String

В каждом платформа-специфичном модуле (iOS и Android) предоставьте фактическую реализацию функции randomUUID() ожидаемой в общем модуле. Используйте ключевое слово actual для обозначения фактической реализации.

Следующие примеры показывают реализацию этого для Android и iOS. Платформенно-специфический код использует ключевое слово actual и ожидаемое имя для функции.

// Android
import java.util.*

actual fun randomUUID() = UUID.randomUUID().toString()
// iOS
import platform.Foundation.NSUUID
        
actual fun randomUUID(): String = NSUUID().UUIDString()

Реализация фреймворка для ведения журнала

Другой пример совместного использования кода и взаимодействия между общим и платформным кодом, JS и JVM в данном случае, в минималистичном фреймворке для ведения журнала:

// Common
enum class LogLevel {
    DEBUG, WARN, ERROR
}

internal expect fun writeLogMessage(message: String, logLevel: LogLevel) 

fun logDebug(message: String) = writeLogMessage(message, LogLevel.DEBUG)
fun logWarn(message: String) = writeLogMessage(message, LogLevel.WARN)
fun logError(message: String) = writeLogMessage(message, LogLevel.ERROR)
// JVM
internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
    println("[$logLevel]: $message")
}

Для JavaScript доступен совершенно другой набор API, и объявление actual будет выглядеть так.

// JS
internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
    when (logLevel) {
        LogLevel.DEBUG -> console.log(message)
        LogLevel.WARN -> console.warn(message)
        LogLevel.ERROR -> console.error(message)
    }
}

Отправка и получение сообщений из WebSocket

Представьте себе разработку платформы для чата для iOS и Android с использованием Kotlin Multiplatform Mobile. Давайте посмотрим, как вы можете реализовать отправку и получение сообщений из WebSocket.

Для этого определите общий код, который вам не нужно дублировать во всех модулях платформ — просто добавьте его один раз в общий модуль. Однако фактическая реализация класса WebSocket отличается от платформы к платформе. Вот почему для этого класса следует использовать объявления expect/actual.

В общем модуле объявите ожидаемый класс PlatformSocket() с ключевым словом expect. Не включайте никакой код реализации.

//Common
internal expect class PlatformSocket(
        url: String
) {
    fun openSocket(listener: PlatformSocketListener)
    fun closeSocket(code: Int, reason: String)
    fun sendMessage(msg: String)
}
interface PlatformSocketListener {
    fun onOpen()
    fun onFailure(t: Throwable)
    fun onMessage(msg: String)
    fun onClosing(code: Int, reason: String)
    fun onClosed(code: Int, reason: String)
}

В каждом платформа-специфичном модуле (iOS и Android) предоставьте фактическую реализацию класса PlatformSocket() ожидаемого в общем модуле. Используйте ключевое слово actual для обозначения фактической реализации.

Следующие примеры показывают реализацию этого для Android и iOS.

//Android
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket

internal actual class PlatformSocket actual constructor(url: String) {
    private val socketEndpoint = url
    private var webSocket: WebSocket? = null
    actual fun openSocket(listener: PlatformSocketListener) {
        val socketRequest = Request.Builder().url(socketEndpoint).build()
        val webClient = OkHttpClient().newBuilder().build()
        webSocket = webClient.newWebSocket(
                socketRequest,
                object : okhttp3.WebSocketListener() {
                    override fun onOpen(webSocket: WebSocket, response: Response) = listener.onOpen()
                    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) = listener.onFailure(t)
                    override fun onMessage(webSocket: WebSocket, text: String) = listener.onMessage(text)
                    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) = listener.onClosing(code, reason)
                    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) = listener.onClosed(code, reason)
                }
        )
    }
    actual fun closeSocket(code: Int, reason: String) {
        webSocket?.close(code, reason)
        webSocket = null
    }
    actual fun sendMessage(msg: String) {
        webSocket?.send(msg)
    }
}

Реализация для Android использует стороннюю библиотеку OkHttp. Добавьте соответствующую зависимость к build.gradle(.kts) в общем модуле:

sourceSets {
    val androidMain by getting {
        dependencies {
            implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
        }
    }
}
commonMain {
    dependencies {
        implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
    }
}

Реализация для iOS использует NSURLSession из стандартного Apple SDK и не требует дополнительных зависимостей.

//iOS
import platform.Foundation.*
import platform.darwin.NSObject

internal actual class PlatformSocket actual constructor(url: String) {
    private val socketEndpoint = NSURL.URLWithString(url)!!
    private var webSocket: NSURLSessionWebSocketTask? = null
    actual fun openSocket(listener: PlatformSocketListener) {
        val urlSession = NSURLSession.sessionWithConfiguration(
                configuration = NSURLSessionConfiguration.defaultSessionConfiguration(),
                delegate = object : NSObject(), NSURLSessionWebSocketDelegateProtocol {
                    override fun URLSession(
                            session: NSURLSession,
                            webSocketTask: NSURLSessionWebSocketTask,
                            didOpenWithProtocol: String?
                    ) {
                        listener.onOpen()
                    }
                    override fun URLSession(
                            session: NSURLSession,
                            webSocketTask: NSURLSessionWebSocketTask,
                            didCloseWithCode: NSURLSessionWebSocketCloseCode,
                            reason: NSData?
                    ) {
                        listener.onClosed(didCloseWithCode.toInt(), reason.toString())
                    }
                },
                delegateQueue = NSOperationQueue.currentQueue()
        )
        webSocket = urlSession.webSocketTaskWithURL(socketEndpoint)
        listenMessages(listener)
        webSocket?.resume()
    }
    private fun listenMessages(listener: PlatformSocketListener) {
        webSocket?.receiveMessageWithCompletionHandler { message, nsError ->
            when {
                nsError != null -> {
                    listener.onFailure(Throwable(nsError.description))
                }
                message != null -> {
                    message.string?.let { listener.onMessage(it) }
                }
            }
            listenMessages(listener)
        }
    }
    actual fun closeSocket(code: Int, reason: String) {
        webSocket?.cancelWithCloseCode(code.toLong(), null)
        webSocket = null
    }
    actual fun sendMessage(msg: String) {
        val message = NSURLSessionWebSocketMessage(msg)
        webSocket?.sendMessage(message) { err ->
            err?.let { println("send $msg error: $it") }
        }
    }
}

И вот общий код в общем модуле, который использует платформа-специфичный класс PlatformSocket().

//Common
class AppSocket(url: String) {
    private val ws = PlatformSocket(url)
    var socketError: Throwable? = null
        private set
    var currentState: State = State.CLOSED
        private set(value) {
            field = value
            stateListener?.invoke(value)
        }
    var stateListener: ((State) -> Unit)? = null
        set(value) {
            field = value
            value?.invoke(currentState)
        }
    var messageListener: ((msg: String) -> Unit)? = null
    fun connect() {
        if (currentState != State.CLOSED) {
            throw IllegalStateException("The socket is available.")
        }
        socketError = null
        currentState = State.CONNECTING
        ws.openSocket(socketListener)
    }
    fun disconnect() {
        if (currentState != State.CLOSED) {
            currentState = State.CLOSING
            ws.closeSocket(1000, "The user has closed the connection.")
        }
    }
    fun send(msg: String) {
        if (currentState != State.CONNECTED) throw IllegalStateException("The connection is lost.")
        ws.sendMessage(msg)
    }
    private val socketListener = object : PlatformSocketListener {
        override fun onOpen() {
            currentState = State.CONNECTED
        }
        override fun onFailure(t: Throwable) {
            socketError = t
            currentState = State.CLOSED
        }
        override fun onMessage(msg: String) {
            messageListener?.invoke(msg)
        }
        override fun onClosing(code: Int, reason: String) {
            currentState = State.CLOSING
        }
        override fun onClosed(code: Int, reason: String) {
            currentState = State.CLOSED
        }
    }
    enum class State {
        CONNECTING,
        CONNECTED,
        CLOSING,
        CLOSED
    }
}

Правила для ожидаемых и фактических объявлений

Основные правила, касающиеся ожидаемых и фактических объявлений:

  • Ожидаемое объявление помечается ключевым словом expect; фактическое объявление помечается ключевым словом actual.

  • Объявления expect и actual имеют одинаковое имя и расположены в одном пакете (имеют одинаковое полное имя).

  • Объявления expect никогда не содержат кода реализации и по умолчанию являются абстрактными.

  • В интерфейсах функции в объявлениях expect не могут иметь тела, но их аналоги actual могут быть не абстрактными и иметь тело. Это позволяет наследникам не реализовывать определенную функцию.

    Чтобы указать, что общие наследники не должны реализовывать функцию, отметьте ее как open. Все ее реализации actual будут обязаны иметь тело:

    // Common
    expect interface Mascot {
        open fun display(): String
    }
    
    class MascotImpl : Mascot {
        // it's ok not to implement `display()`: all `actual`s are guaranteed to have a default implementation
    }
    
    // Platform-specific
    actual interface Mascot {
        actual fun display(): String {
            TODO()
        }
    }
    

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

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

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>
Последнее изменение: 28 февраля 2022 г.
Обмен кодом на платформах Иерархическая структура проекта

© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform-connect-to-apis.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API