How Platform-Specific Code Is Implemented Using Kotlin's expect/actual Pattern in the Muse Repository

The Muse repository leverages Kotlin Multiplatform's expect/actual mechanism to declare abstract contracts in commonMain and provide concrete Android and iOS implementations in androidMain and iosMain, isolating native SDK dependencies while maximizing code reuse.

The Muse project is a Kotlin Multiplatform application targeting Android and iOS. To access platform-specific capabilities—such as file system paths, native logging, and UI components—without fragmenting the business logic, the codebase adopts the Kotlin expect/actual pattern. This architectural strategy defines lightweight API contracts in the shared module and fulfills them with platform-specific implementations in dedicated source sets.

Declaring Platform Contracts in commonMain

The shared module (commonMain) contains pure Kotlin declarations that describe what the platform must provide, not how to provide it. These declarations use the expect keyword and act as compile-time boundaries between shared and platform code.

Platform Detection and Constants

In src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/Platform.kt, the repository defines an enum-based platform identifier:

expect val CURRENT_PLATFORM: Platform

enum class Platform {
    Android, Ios
}

This allows shared code to branch behavior based on the runtime platform while remaining testable on the JVM.

File System Abstractions

Path management is abstracted through src/commonMain/kotlin/io/github/kkoshin/muse/repo/MusePathManager.kt:

expect class MusePathManager {
    fun getCacheDir(): String
    fun getExportDir(): String
    fun getTempDir(): String
}

Additionally, src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/Platform.kt exposes expect val SystemFileSystem: FileSystem (from Okio) for low-level file operations.

Database Driver Factory

For local data persistence, src/commonMain/kotlin/io/github/kkoshin/muse/repo/SqlDriver.kt declares:

expect class DriverFactory {
    fun createDriver(): SqlDriver
}

This enables SQLDelight usage across platforms without exposing platform-specific driver initialization to the shared domain layer.

Platform Information and Logging

UI and diagnostic utilities follow the same pattern. src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/PlatformInfo.kt defines:

expect fun rememberPlatformSpecificInfo(): PlatformSpecificInfo

interface PlatformSpecificInfo {
    val versionName: String
    val versionCode: Int
    val exportFolderPath: String
    fun openUrl(url: String)
}

Similarly, src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/logcat.kt provides a unified logging interface:

expect inline fun logcat(tag: String = "Muse", block: () -> String)

Android Implementations in androidMain

The androidMain source set supplies actual implementations that bridge to the Android SDK.

Platform and Path Resolution

In src/androidMain/kotlin/io/github/kkoshin/muse/platformbridge/Platform.android.kt:

actual val CURRENT_PLATFORM: Platform = Platform.Android

The file manager implementation in src/androidMain/kotlin/io/github/kkoshin/muse/repo/MusePathManager.android.kt resolves paths via Context:

actual class MusePathManager(private val context: Context) {
    actual fun getCacheDir(): String = context.cacheDir.absolutePath
    actual fun getExportDir(): String = context.getExternalFilesDir(null)?.absolutePath 
        ?: context.filesDir.absolutePath
    actual fun getTempDir(): String = context.cacheDir.absolutePath
}

Database and System Services

The SQLDelight driver is instantiated in src/androidMain/kotlin/io/github/kkoshin/muse/repo/SqlDriver.android.kt:

actual class DriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver {
        return AndroidSqlDriver(Database.Schema, context, "muse.db")
    }
}

Platform information and logging are handled in src/androidMain/kotlin/io/github/kkoshin/muse/platformbridge/PlatformInfo.android.kt and src/androidMain/kotlin/io/github/kkoshin/muse/platformbridge/logcat.android.kt, respectively. The former reads BuildConfig.VERSION_NAME and launches URLs via Intent.ACTION_VIEW, while the latter delegates to android.util.Log.d.

iOS Implementations in iosMain

The iosMain source set provides counterparts using Kotlin/Native interop with Apple frameworks.

Platform Identification and File Management

src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/Platform.ios.kt defines:

actual val CURRENT_PLATFORM: Platform = Platform.Ios

Path resolution in src/iosMain/kotlin/io/github/kkoshin/muse/repo/MusePathManager.ios.kt utilizes NSFileManager:

actual class MusePathManager {
    actual fun getCacheDir(): String = NSTemporaryDirectory()
    actual fun getExportDir(): String = NSFileManager.defaultManager.URLForDirectory(
        NSDocumentDirectory, NSUserDomainMask, null, true, null
    )?.path ?: ""
    actual fun getTempDir(): String = NSTemporaryDirectory()
}

Native UI and Diagnostics

src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/PlatformInfo.ios.kt implements version retrieval via NSBundle.mainBundle and URL opening with SFSafariViewController or UIApplication.sharedApplication.openURL. Logging in src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/logcat.ios.kt simplifies to:

actual inline fun logcat(tag: String, block: () -> String) {
    println("[$tag] ${block()}")
}

The SQL driver factory in src/iosMain/kotlin/io/github/kkoshin/muse/repo/DriverFactory.ios.kt returns a native SqlDriver configured for iOS SQLite.

Consuming Platform APIs in Shared Code

Shared domain classes consume these abstractions without import statements for platform classes. For example, a version reporting service in commonMain remains pure Kotlin:

class VersionReporter {
    private val info = rememberPlatformSpecificInfo()
    
    fun reportVersion() {
        logcat { "App version: ${info.versionName} (${info.versionCode})" }
    }
}

During compilation, the Kotlin compiler substitutes the appropriate actual implementation based on the target platform. The shared code invokes rememberPlatformSpecificInfo() and logcat() without knowing whether it will execute on Android or iOS.

Summary

  • Abstract Contracts: The commonMain module defines expect declarations for platform detection, file paths, database drivers, logging, and UI helpers in files like Platform.kt, MusePathManager.kt, and PlatformInfo.kt.
  • Android Fulfillment: The androidMain source set provides actual implementations in Platform.android.kt, MusePathManager.android.kt, and SqlDriver.android.kt, bridging to Android SDK classes such as Context and SQLiteOpenHelper.
  • iOS Fulfillment: The iosMain source set supplies actual implementations in Platform.ios.kt, MusePathManager.ios.kt, and DriverFactory.ios.kt, utilizing Kotlin/Native interop with NSFileManager, NSBundle, and native SQLite.
  • Zero Platform Leakage: Shared business logic references only the expect APIs, ensuring compile-time safety and enabling unit testing on the JVM without mobile emulators.

Frequently Asked Questions

What is the purpose of the expect/actual pattern in Kotlin Multiplatform?

The expect/actual pattern allows developers to define a common API surface in shared Kotlin code using the expect keyword, then provide platform-specific implementations using the actual keyword in platform-specific source sets. This mechanism enables the shared module to remain pure Kotlin while delegating to native SDKs like Android's Context or iOS's Foundation framework.

How does Muse handle file system differences between Android and iOS?

Muse abstracts file system access through the MusePathManager class. In commonMain, it is declared as an expect class with methods like getCacheDir() and getExportDir(). The androidMain implementation uses Context.getExternalFilesDir() and Context.cacheDir, while the iosMain implementation uses NSFileManager and NSTemporaryDirectory(), allowing shared code to manipulate paths uniformly.

Can the Muse codebase support additional platforms like Desktop or Web?

Yes. Because the platform-specific contracts are isolated in discrete expect declarations, adding support for JVM Desktop or JavaScript would require creating new source sets (e.g., desktopMain or jsMain) and providing actual implementations for each expect function and class. The existing commonMain logic would remain unchanged.

Where are the platform-specific implementations located in the repository?

Android implementations reside under src/androidMain/kotlin/io/github/kkoshin/muse/, typically in files suffixed with .android.kt (e.g., Platform.android.kt, MusePathManager.android.kt). iOS implementations are located under src/iosMain/kotlin/io/github/kkoshin/muse/ with the .ios.kt suffix (e.g., Platform.ios.kt, PlatformInfo.ios.kt).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →