Skip to main content

Kotlin Expect/Actual

Introduction

When developing multiplatform applications with Kotlin, you'll often need to access platform-specific APIs while maintaining a shared codebase. This is where Kotlin's expect/actual mechanism comes into play. It allows you to define a common API in shared code (with expect declarations) and provide platform-specific implementations (with actual declarations) for each target platform.

Think of the expect/actual mechanism as Kotlin's way of saying: "I expect this functionality to exist, but the actual implementation will be provided separately for each platform."

Understanding Expect/Actual Declarations

What Are Expect Declarations?

An expect declaration defines what should be available in the common code, but doesn't provide implementation details. It's essentially a contract that platform-specific code must fulfill.

What Are Actual Declarations?

An actual declaration provides the platform-specific implementation of an expect declaration. Each target platform must provide an implementation for every expect declaration in the common code.

Basic Syntax

Here's the basic syntax for expect/actual declarations:

// In common code
expect fun platformName(): String

// In Android-specific code
actual fun platformName(): String = "Android"

// In iOS-specific code
actual fun platformName(): String = "iOS"

// In JS-specific code
actual fun platformName(): String = "JavaScript"

What Can Be Declared with Expect/Actual?

You can use the expect/actual mechanism with:

  • Functions
  • Properties
  • Classes
  • Objects
  • Typealias

Let's examine each of these in detail.

Expect/Actual with Functions

Functions are perhaps the most common use case for expect/actual declarations.

// In common code
expect fun getCurrentDateTime(): String

// In JVM-specific code
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

actual fun getCurrentDateTime(): String {
val current = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return current.format(formatter)
}

// In JS-specific code
actual fun getCurrentDateTime(): String {
val date = js("new Date()")
return date.toLocaleString()
}

When you call getCurrentDateTime() in your common code, you'll get the platform-specific implementation at runtime.

Expect/Actual with Properties

Properties can also use the expect/actual mechanism:

// In common code
expect val platform: String

// In Android-specific code
actual val platform: String = "Android"

// In iOS-specific code
actual val platform: String = "iOS"

Expect/Actual with Classes

Classes can be declared with expect/actual to define platform-specific implementations:

// In common code
expect class FileManager {
fun readFile(path: String): String
fun writeFile(path: String, content: String)
}

// In JVM-specific code
import java.io.File

actual class FileManager {
actual fun readFile(path: String): String {
return File(path).readText()
}

actual fun writeFile(path: String, content: String) {
File(path).writeText(content)
}
}

// In JS-specific code
actual class FileManager {
actual fun readFile(path: String): String {
val fs = js("require('fs')")
return fs.readFileSync(path, "utf8")
}

actual fun writeFile(path: String, content: String) {
val fs = js("require('fs')")
fs.writeFileSync(path, content)
}
}

Expect/Actual with Objects

Singleton objects can also be declared with expect/actual:

// In common code
expect object Logger {
fun debug(message: String)
fun error(message: String)
}

// In JVM-specific code
actual object Logger {
actual fun debug(message: String) {
println("DEBUG: $message")
}

actual fun error(message: String) {
System.err.println("ERROR: $message")
}
}

// In JS-specific code
actual object Logger {
actual fun debug(message: String) {
js("console.log('DEBUG: ' + message)")
}

actual fun error(message: String) {
js("console.error('ERROR: ' + message)")
}
}

Expect/Actual with Typealias

You can use typealias with expect/actual to map to platform-specific types:

// In common code
expect class PlatformDate

// In JVM-specific code
import java.util.Date
actual typealias PlatformDate = Date

// In JS-specific code
actual typealias PlatformDate = js.Date

Real-World Example: HTTP Client

Let's build a simple HTTP client using expect/actual:

// In common code
expect class HttpClient() {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}

// Usage in common code
suspend fun fetchUserData(userId: String): User {
val client = HttpClient()
val response = client.get("https://api.example.com/users/$userId")
return parseUserJson(response)
}

// In JVM-specific code
import java.net.URL

actual class HttpClient {
actual suspend fun get(url: String): String {
return URL(url).readText()
}

actual suspend fun post(url: String, body: String): String {
val connection = URL(url).openConnection()
connection.doOutput = true
connection.requestMethod = "POST"
connection.setRequestProperty("Content-Type", "application/json")

connection.outputStream.use { os ->
os.write(body.toByteArray())
}

return connection.inputStream.bufferedReader().use { it.readText() }
}
}

// In JS-specific code
actual class HttpClient {
actual suspend fun get(url: String): String {
val fetch = js("fetch")
val response = fetch(url).await()
return response.text().await()
}

actual suspend fun post(url: String, body: String): String {
val fetch = js("fetch")
val options = js("{}")
options.method = "POST"
options.headers = js("{}")
options.headers["Content-Type"] = "application/json"
options.body = body

val response = fetch(url, options).await()
return response.text().await()
}
}

Best Practices

  1. Keep expect declarations minimal: Define only what's necessary in your expect declarations to maintain a clean API.

  2. Use platform-specific code sparingly: Overusing expect/actual can make your codebase harder to maintain. Use it only when necessary.

  3. Consistent naming: Use the same property and parameter names in both expect and actual declarations.

  4. Document expect declarations: Include documentation comments on your expect declarations to make it clear what the implementation should do.

  5. Consider default implementations: For simple cases, you might implement the functionality in common code and only override it on specific platforms when necessary.

Common Pitfalls

  1. Missing actual declarations: If you forget to provide an actual declaration for any platform, your code won't compile for that platform.

  2. Signature mismatch: The signatures of expect and actual declarations must match exactly, including nullability, default parameters, etc.

  3. Visibility modifiers: The visibility of actual declarations must match or be less restrictive than the expect declaration.

Example Project Structure

For a typical Kotlin Multiplatform project, your files might be organized like this:

src/
├── commonMain/
│ └── kotlin/
│ └── com/example/
│ └── DateUtils.kt (contains expect declarations)
├── jvmMain/
│ └── kotlin/
│ └── com/example/
│ └── DateUtils.kt (contains JVM actual implementations)
├── jsMain/
│ └── kotlin/
│ └── com/example/
│ └── DateUtils.kt (contains JS actual implementations)
└── iosMain/
└── kotlin/
└── com/example/
└── DateUtils.kt (contains iOS actual implementations)

Summary

Kotlin's expect/actual mechanism is a powerful feature that allows you to:

  1. Define a common API in your shared code
  2. Implement platform-specific versions of that API
  3. Use platform-specific libraries and frameworks while maintaining a shared codebase

This approach gives you the flexibility to access native platform capabilities while maximizing code sharing across platforms. It's one of the key features that makes Kotlin Multiplatform a practical approach to cross-platform development.

Additional Resources

Exercises

  1. Create a simple multiplatform project with an expect/actual function that returns the current platform name.

  2. Implement a file storage class with expect/actual that can read and write simple text files on different platforms.

  3. Create a platform-specific logger that uses Android's LogCat on Android, Console on JVM, and console.log on JavaScript.

  4. Build a simple date formatter with expect/actual declarations that formats dates according to platform-specific conventions.

  5. Create an expect class for accessing device information (like screen size, device model, etc.) and implement it for at least two platforms.

💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!