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.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)