接入平台相关 API

The expect/actual feature is in Beta. It is almost stable, but migration steps may be required in the future. We’ll do our best to minimize any changes you will have to make.

接入平台相关 API - 图1

If you’re developing a multiplatform application that needs to access platform-specific APIs that implement the required functionality (for example, generating a UUID), use the Kotlin mechanism of expected and actual declarations.

With this mechanism, a common source set defines an expected declaration, and platform source sets must provide the actual declaration that corresponds to the expected declaration. This works for most Kotlin declarations, such as functions, classes, interfaces, enumerations, properties, and annotations.

Expect/actual declarations in common and platform-specific modules

The compiler ensures that every declaration marked with the expect keyword in the common module has the corresponding declarations marked with the actual keyword in all platform modules. The IDE provides tools that help you create the missing actual declarations.

Use expected and actual declarations only for Kotlin declarations that have platform-specific dependencies. Implementing as much functionality as possible in the shared module is better, even if doing so takes more time.

Don’t overuse expected and actual declarations – in some cases, an interface may be a better choice because it is more flexible and easier to test.

接入平台相关 API - 图3

Learn how to add dependencies on platform-specific libraries.

Examples

For simplicity, the following examples use intuitive target names, like iOS and Android. However, in your Gradle build files, you need to use a specific target name from the list of supported targets.

Generate a UUID

Let’s assume that you are developing iOS and Android applications using Kotlin Multiplatform and you want to generate a universally unique identifier (UUID):

Expect/actual declarations for getting the UUID

For this purpose, declare the expected function randomUUID() with the expect keyword in the common module. Don’t include any implementation code.

  1. // Common
  2. expect fun randomUUID(): String

In each platform-specific module (iOS and Android), provide the actual implementation for the function randomUUID() expected in the common module. Use the actual keyword to mark the actual implementation.

The following examples show the implementation of this for Android and iOS. Platform-specific code uses the actual keyword and the expected name for the function.

  1. // Android
  2. import java.util.*
  3. actual fun randomUUID() = UUID.randomUUID().toString()
  1. // iOS
  2. import platform.Foundation.NSUUID
  3. actual fun randomUUID(): String = NSUUID().UUIDString()

Implement a logging framework

Another example of code sharing and interaction between the common and platform logic, JS and JVM in this case, in a minimalistic logging framework:

  1. // Common
  2. enum class LogLevel {
  3. DEBUG, WARN, ERROR
  4. }
  5. internal expect fun writeLogMessage(message: String, logLevel: LogLevel)
  6. fun logDebug(message: String) = writeLogMessage(message, LogLevel.DEBUG)
  7. fun logWarn(message: String) = writeLogMessage(message, LogLevel.WARN)
  8. fun logError(message: String) = writeLogMessage(message, LogLevel.ERROR)
  1. // JVM
  2. internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
  3. println("[$logLevel]: $message")
  4. }

For JavaScript, a completely different set of APIs is available, and the actual declaration will look like this.

  1. // JS
  2. internal actual fun writeLogMessage(message: String, logLevel: LogLevel) {
  3. when (logLevel) {
  4. LogLevel.DEBUG -> console.log(message)
  5. LogLevel.WARN -> console.warn(message)
  6. LogLevel.ERROR -> console.error(message)
  7. }
  8. }

Send and receive messages from a WebSocket

Consider developing a chat platform for iOS and Android using Kotlin Multiplatform. Let’s see how you can implement sending and receiving messages from a WebSocket.

For this purpose, define a common logic that you don’t need to duplicate in all platform modules – just add it once to the common module. However, the actual implementation of the WebSocket class differs from platform to platform. That’s why you should use expect/actual declarations for this class.

In the common module, declare the expected class PlatformSocket() with the expect keyword. Don’t include any implementation code.

  1. //Common
  2. internal expect class PlatformSocket(
  3. url: String
  4. ) {
  5. fun openSocket(listener: PlatformSocketListener)
  6. fun closeSocket(code: Int, reason: String)
  7. fun sendMessage(msg: String)
  8. }
  9. interface PlatformSocketListener {
  10. fun onOpen()
  11. fun onFailure(t: Throwable)
  12. fun onMessage(msg: String)
  13. fun onClosing(code: Int, reason: String)
  14. fun onClosed(code: Int, reason: String)
  15. }

In each platform-specific module (iOS and Android), provide the actual implementation for the class PlatformSocket() expected in the common module. Use the actual keyword to mark the actual implementation.

The following examples show the implementation of this for Android and iOS.

  1. //Android
  2. import okhttp3.OkHttpClient
  3. import okhttp3.Request
  4. import okhttp3.Response
  5. import okhttp3.WebSocket
  6. internal actual class PlatformSocket actual constructor(url: String) {
  7. private val socketEndpoint = url
  8. private var webSocket: WebSocket? = null
  9. actual fun openSocket(listener: PlatformSocketListener) {
  10. val socketRequest = Request.Builder().url(socketEndpoint).build()
  11. val webClient = OkHttpClient().newBuilder().build()
  12. webSocket = webClient.newWebSocket(
  13. socketRequest,
  14. object : okhttp3.WebSocketListener() {
  15. override fun onOpen(webSocket: WebSocket, response: Response) = listener.onOpen()
  16. override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) = listener.onFailure(t)
  17. override fun onMessage(webSocket: WebSocket, text: String) = listener.onMessage(text)
  18. override fun onClosing(webSocket: WebSocket, code: Int, reason: String) = listener.onClosing(code, reason)
  19. override fun onClosed(webSocket: WebSocket, code: Int, reason: String) = listener.onClosed(code, reason)
  20. }
  21. )
  22. }
  23. actual fun closeSocket(code: Int, reason: String) {
  24. webSocket?.close(code, reason)
  25. webSocket = null
  26. }
  27. actual fun sendMessage(msg: String) {
  28. webSocket?.send(msg)
  29. }
  30. }

Android implementation uses the third-party library OkHttp. Add the corresponding dependency to build.gradle(.kts) in the shared module:

【Kotlin】

  1. sourceSets {
  2. val androidMain by getting {
  3. dependencies {
  4. implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
  5. }
  6. }
  7. }

【Groovy】

  1. commonMain {
  2. dependencies {
  3. implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
  4. }
  5. }

iOS implementation uses NSURLSession from the standard Apple SDK and doesn’t require additional dependencies.

  1. //iOS
  2. import platform.Foundation.*
  3. import platform.darwin.NSObject
  4. internal actual class PlatformSocket actual constructor(url: String) {
  5. private val socketEndpoint = NSURL.URLWithString(url)!!
  6. private var webSocket: NSURLSessionWebSocketTask? = null
  7. actual fun openSocket(listener: PlatformSocketListener) {
  8. val urlSession = NSURLSession.sessionWithConfiguration(
  9. configuration = NSURLSessionConfiguration.defaultSessionConfiguration(),
  10. delegate = object : NSObject(), NSURLSessionWebSocketDelegateProtocol {
  11. override fun URLSession(
  12. session: NSURLSession,
  13. webSocketTask: NSURLSessionWebSocketTask,
  14. didOpenWithProtocol: String?
  15. ) {
  16. listener.onOpen()
  17. }
  18. override fun URLSession(
  19. session: NSURLSession,
  20. webSocketTask: NSURLSessionWebSocketTask,
  21. didCloseWithCode: NSURLSessionWebSocketCloseCode,
  22. reason: NSData?
  23. ) {
  24. listener.onClosed(didCloseWithCode.toInt(), reason.toString())
  25. }
  26. },
  27. delegateQueue = NSOperationQueue.currentQueue()
  28. )
  29. webSocket = urlSession.webSocketTaskWithURL(socketEndpoint)
  30. listenMessages(listener)
  31. webSocket?.resume()
  32. }
  33. private fun listenMessages(listener: PlatformSocketListener) {
  34. webSocket?.receiveMessageWithCompletionHandler { message, nsError ->
  35. when {
  36. nsError != null -> {
  37. listener.onFailure(Throwable(nsError.description))
  38. }
  39. message != null -> {
  40. message.string?.let { listener.onMessage(it) }
  41. }
  42. }
  43. listenMessages(listener)
  44. }
  45. }
  46. actual fun closeSocket(code: Int, reason: String) {
  47. webSocket?.cancelWithCloseCode(code.toLong(), null)
  48. webSocket = null
  49. }
  50. actual fun sendMessage(msg: String) {
  51. val message = NSURLSessionWebSocketMessage(msg)
  52. webSocket?.sendMessage(message) { err ->
  53. err?.let { println("send $msg error: $it") }
  54. }
  55. }
  56. }

And here is the common logic in the common module that uses the platform-specific class PlatformSocket().

  1. //Common
  2. class AppSocket(url: String) {
  3. private val ws = PlatformSocket(url)
  4. var socketError: Throwable? = null
  5. private set
  6. var currentState: State = State.CLOSED
  7. private set(value) {
  8. field = value
  9. stateListener?.invoke(value)
  10. }
  11. var stateListener: ((State) -> Unit)? = null
  12. set(value) {
  13. field = value
  14. value?.invoke(currentState)
  15. }
  16. var messageListener: ((msg: String) -> Unit)? = null
  17. fun connect() {
  18. if (currentState != State.CLOSED) {
  19. throw IllegalStateException("The socket is available.")
  20. }
  21. socketError = null
  22. currentState = State.CONNECTING
  23. ws.openSocket(socketListener)
  24. }
  25. fun disconnect() {
  26. if (currentState != State.CLOSED) {
  27. currentState = State.CLOSING
  28. ws.closeSocket(1000, "The user has closed the connection.")
  29. }
  30. }
  31. fun send(msg: String) {
  32. if (currentState != State.CONNECTED) throw IllegalStateException("The connection is lost.")
  33. ws.sendMessage(msg)
  34. }
  35. private val socketListener = object : PlatformSocketListener {
  36. override fun onOpen() {
  37. currentState = State.CONNECTED
  38. }
  39. override fun onFailure(t: Throwable) {
  40. socketError = t
  41. currentState = State.CLOSED
  42. }
  43. override fun onMessage(msg: String) {
  44. messageListener?.invoke(msg)
  45. }
  46. override fun onClosing(code: Int, reason: String) {
  47. currentState = State.CLOSING
  48. }
  49. override fun onClosed(code: Int, reason: String) {
  50. currentState = State.CLOSED
  51. }
  52. }
  53. enum class State {
  54. CONNECTING,
  55. CONNECTED,
  56. CLOSING,
  57. CLOSED
  58. }
  59. }

Rules for expected and actual declarations

The main rules regarding expected and actual declarations are:

  • An expected declaration is marked with the expect keyword; the actual declaration is marked with the actual keyword.
  • expect and actual declarations have the same name and are located in the same package (have the same fully qualified name).
  • expect declarations never contain any implementation code and are abstract by default.
  • In interfaces, functions in expect declarations cannot have bodies, but their actual counterparts can be non-abstract and have a body. It allows the inheritors not to implement a particular function.

    To indicate that common inheritors don’t need to implement a function, mark it as open. All its actual implementations will be required to have a body:

    1. // Common
    2. expect interface Mascot {
    3. open fun display(): String
    4. }
    5. class MascotImpl : Mascot {
    6. // it's ok not to implement `display()`: all `actual`s are guaranteed to have a default implementation
    7. }
    8. // Platform-specific
    9. actual interface Mascot {
    10. actual fun display(): String {
    11. TODO()
    12. }
    13. }

During each platform compilation, the compiler ensures that every declaration marked with the expect keyword in the common or intermediate source set has the corresponding declarations marked with the actual keyword in all platform source sets. The IDE provides tools that help you create the missing actual declarations.

If you have a platform-specific library that you want to use in shared code while providing your own implementation for another platform, you can provide a typealias to an existing class as the actual declaration:

  1. expect class AtomicRef<V>(value: V) {
  2. fun get(): V
  3. fun set(value: V)
  4. fun getAndSet(value: V): V
  5. fun compareAndSet(expect: V, update: V): Boolean
  6. }
  1. actual typealias AtomicRef<V> = java.util.concurrent.atomic.AtomicReference<V>