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
-
Keep expect declarations minimal: Define only what's necessary in your expect declarations to maintain a clean API.
-
Use platform-specific code sparingly: Overusing expect/actual can make your codebase harder to maintain. Use it only when necessary.
-
Consistent naming: Use the same property and parameter names in both expect and actual declarations.
-
Document expect declarations: Include documentation comments on your expect declarations to make it clear what the implementation should do.
-
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
-
Missing actual declarations: If you forget to provide an actual declaration for any platform, your code won't compile for that platform.
-
Signature mismatch: The signatures of expect and actual declarations must match exactly, including nullability, default parameters, etc.
-
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:
- Define a common API in your shared code
- Implement platform-specific versions of that API
- 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
- Kotlin Multiplatform Official Documentation
- Kotlin Multiplatform: Platform-Specific Declarations
- KMP Samples Repository
Exercises
-
Create a simple multiplatform project with an expect/actual function that returns the current platform name.
-
Implement a file storage class with expect/actual that can read and write simple text files on different platforms.
-
Create a platform-specific logger that uses Android's LogCat on Android, Console on JVM, and console.log on JavaScript.
-
Build a simple date formatter with expect/actual declarations that formats dates according to platform-specific conventions.
-
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!