Expect and Actual in KMP

How expect and actual Work in Kotlin

Codefy Labs's photo
·

3 min read

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

  1. 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
    
  2. 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

  1. Define Expected Declarations: In your common module, declare a standard Kotlin construct (function, property, class, etc.) with the expect keyword.

  2. 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:

  1. Common Code (commonMain):

     package identity
    
     class Identity(val userName: String, val processID: Long)
     expect fun buildIdentity(): Identity
    
  2. 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()
     )
    
  3. 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:

  1. Common Code:

     interface Identity {
         val userName: String
         val processID: Long
     }
     expect fun buildIdentity(): Identity
    
  2. JVM Implementation:

     actual fun buildIdentity(): Identity = object : Identity {
         override val userName: String = System.getProperty("user.name") ?: "none"
         override val processID: Long = ProcessHandle.current().pid()
     }
    
  3. Native Implementation:

     actual fun buildIdentity(): Identity = object : Identity {
         override val userName: String = getlogin()?.toKString() ?: "None"
         override val processID: Long = getpid().toLong()
     }
    

Practical Tips

  1. Keep Declarations Simple: Use interfaces and factory functions to minimize complexity.

  2. Leverage IDE Support: Modern IDEs provide tools to help you navigate and ensure your expect and actual declarations are correctly matched.

  3. 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!