Подключение к платформам-специфичным API
Если вы разрабатываете многоплатформенное приложение, которому необходимо получить доступ к платформам-специфичным API, которые реализуют необходимую функциональность (например, генерации UUID), используйте механизм Kotlin для ожидаемых и фактических объявлений.
С помощью этого механизма, общий набор исходных файлов определяет ожидаемое объявление, а наборы исходных файлов платформ должны предоставить фактическое объявление, соответствующее ожидаемому объявлению. Это работает для большинства объявлений Kotlin, таких как функции, классы, интерфейсы, перечисления, свойства и аннотации.
Компилятор гарантирует, что каждое объявление, помеченное ключевым словом expect в общем модуле, имеет соответствующие объявления, помеченные ключевым словом actual во всех модулях платформ. IDE предоставляет инструменты, которые помогут вам создать недостающие фактические объявления.
Узнайте, как добавить зависимости от платформа-специфичных библиотек.
Примеры
Для простоты в следующих примерах используются интуитивные имена целей, такие как iOS и Android. Однако в ваших файлах Gradle вам необходимо использовать конкретное имя цели из списка поддерживаемых целей.
Генерация UUID
Предположим, что вы разрабатываете приложения iOS и Android с помощью Kotlin Multiplatform Mobile и хотите сгенерировать универсальный уникальный идентификатор (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>
© 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