Kotlin Multiplatform is a powerful tool that allows you to write shared code across multiple platforms while still accessing platform-specific APIs. The key to this magic lies in the expect
and actual
declarations. Here’s a practical guide to understanding and effectively using these declarations in your Kotlin projects.
Understanding expect
and actual
in Simple Terms
Imagine you're writing a recipe book for a global audience. Each region has specific ingredients, but the overall dish remains the same. You describe the recipe in general terms, and local chefs adapt it with their ingredients.
expect
and actual
work similarly in programming:
expect
: In a shared codebase, you define what you need (like a general recipe).actual
: In platform-specific code, you provide the exact details (like local ingredients).
How It Works
Common Code (Shared Recipe): You define what you need using
expect
. This acts as a placeholder.// Common code (shared across platforms) expect fun getUserName(): String
Platform-Specific Code (Local Ingredients): Each platform (like Android, iOS) provides its own implementation using
actual
.// Android implementation actual fun getUserName(): String { return "Android User" } // iOS implementation actual fun getUserName(): String { return "iOS User" }
Example in Practice
Common Module: Defines a function that will return the username.
// Common code (shared) expect fun getUserName(): String
Android Module: Provides the Android-specific way to get the username.
// Android-specific code actual fun getUserName(): String { return "Android User" }
iOS Module: Provides the iOS-specific way to get the username.
// iOS-specific code actual fun getUserName(): String { return "iOS User" }
Basic Rules
Define Expected Declarations: In your common module, declare a standard Kotlin construct (function, property, class, etc.) with the
expect
keyword.Provide Actual Implementations: In each platform-specific module, define the same construct with the
actual
keyword.
Example: Creating an Identity Type
Suppose you need an Identity
type that includes the user’s login name and the process ID. Here’s how you can do it:
Common Code (
commonMain
):package identity class Identity(val userName: String, val processID: Long) expect fun buildIdentity(): Identity
JVM Implementation (
jvmMain
):package identity import java.lang.System import java.lang.ProcessHandle actual fun buildIdentity() = Identity( System.getProperty("user.name") ?: "None", ProcessHandle.current().pid() )
Native Implementation (
nativeMain
):package identity import kotlinx.cinterop.toKString import platform.posix.getlogin import platform.posix.getpid actual fun buildIdentity() = Identity( getlogin()?.toKString() ?: "None", getpid().toLong() )
Using Interfaces for Flexibility
If your factory function becomes too complex, use an interface for the common definition:
Common Code:
interface Identity { val userName: String val processID: Long } expect fun buildIdentity(): Identity
JVM Implementation:
actual fun buildIdentity(): Identity = object : Identity { override val userName: String = System.getProperty("user.name") ?: "none" override val processID: Long = ProcessHandle.current().pid() }
Native Implementation:
actual fun buildIdentity(): Identity = object : Identity { override val userName: String = getlogin()?.toKString() ?: "None" override val processID: Long = getpid().toLong() }
Practical Tips
Keep Declarations Simple: Use interfaces and factory functions to minimize complexity.
Leverage IDE Support: Modern IDEs provide tools to help you navigate and ensure your
expect
andactual
declarations are correctly matched.Handle Platform Differences Gracefully: Use Kotlin's language features to manage different platform capabilities effectively.
Advanced Use Cases
Type Aliases: Use type aliases for actual declarations that need to reference existing platform-specific types.
Visibility Adjustments: Actual implementations can have broader visibility than their expected counterparts.
Handling Enumerations: Platform-specific enumerations can include extra constants, but ensure you handle them in your common code.
By following these guidelines and examples, you can harness the full power of Kotlin Multiplatform to write clean, maintainable, and cross-platform Kotlin code. Whether you're building for JVM, iOS, Android, or other platforms, expect
and actual
declarations are your tools for seamless integration. Happy coding!