Compose Multiplatform in Action: Using expect and actual to Implement Cross-Platform Code
Compose Multiplatform (CMP)
By sharing code
we not only reduce duplicate work
but also improve development efficiency and code consistency
CMP
provides developers with a cross-platform solution
allowing the same business logic to run on different platforms
In this article
we’ll explore how CMP
uses the expect
and actual
keywords to implement cross-platform
code
and share some practical experiences
目錄
- Compose Multiplatform 實戰:放輕鬆點,初探CMP
- Compose Multiplatform 實戰:初戰,安裝CMP環境吧
- Compose Multiplatform 實戰:續戰,用Wizard創建CMP專案
- Compose Multiplatform 實戰:在Android、iOS模擬器上跑CMP專案
- Compose Multiplatform 實戰:CMP的專案結構理解與編譯配置
- Compose Multiplatform 實戰:CMP中跨平台Android、iOS程式碼的進入點
- Compose Multiplatform 實戰:在CMP的Compose中用Material Design3 Theme
- Compose Multiplatform 實戰:CMP用Compose實作跨平台畫面
- Compose Multiplatform 實戰:使用 expect 和 actual 實現跨平台程式碼
- Compose Multiplatform 實戰:CMP中實作Compose Navigation頁面切換
- Compose Multiplatform 實戰:CMP中透過StateFlow來管理UI狀態
- Compose Multiplatform 實戰:CMP中實作NavigationBar底部欄
- Compose Multiplatform 實戰:CMP中使用koin來依賴注入Dependency Injection
- Compose Multiplatform 實戰:CMP實作跨平台資料庫SqlDelight
- Compose Multiplatform 實戰:CMP中使用ROOM開發跨平台資料庫 & 疑難雜症
Let’s first get a basic understanding of Compose Multiplatform and Kotlin Multiplatform
- The
expect
keyword is used to declare a platform-dependent interface or class in shared code
while theactual
keyword is used to implement this interface or class on specific platforms
Let’s look at a simple example
The code below shows how to return the name of the platform on each platform
In commonMain
, we use the expect
keyword to declare a function
and expect other platforms in the CMP project to implement this function
// in ~/commonMain/.../xxx.kt
expect fun getPlatformName(): String
// in ~/androidMain/.../xxx.kt
actual fun getPlatformName(): String {
return "Android"
}
// in ~/iosMain/.../xxx.kt
actual fun getPlatformName(): String {
return "iOS"
}
Additionally
expect
and actual
can be used not only for functions
but also for classes
This allows us to flexibly define platform-specific logic
in shared code
and provide implementations on specific platforms
// in ~/commonMain/.../FileSystem.kt
expect class FileSystem {
fun readFile(path: String): String
}
// in ~/androidMain/.../FileSystem.kt
actual class FileSystem {
actual fun readFile(path: String): String {
// Android-specific file reading logic
}
}
// in ~/iosMain/.../FileSystem.kt
actual class FileSystem {
actual fun readFile(path: String): String {
// iOS-specific file reading logic
}
}
<div class=”c-border-main-title-2”“>Practical Examples</div>
- For instance, when setting up the material 3 theme a couple of days ago
we set up anexpect
function called setStatusBarStyle
mainlyexpecting a cross-platform function
that can set the status bar on specific platforms
// in ~/commonMain/.../xxxx.kt
@Composable
expect fun setStatusBarStyle(
backgroundColor: Color,
isDarkTheme: Boolean
)
// in ~/androidMain/.../xxx.kt
@Composable
actual fun setStatusBarStyle(backgroundColor: Color, isDarkTheme: Boolean) {
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = backgroundColor.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
}
}
}
// in ~/iosMain/.../xxx.kt
@Composable
actual fun setStatusBarStyle(backgroundColor: Color, isDarkTheme: Boolean) {
DisposableEffect(isDarkTheme) {
val statusBarStyle = if (isDarkTheme) {
UIStatusBarStyleLightContent
} else {
UIStatusBarStyleDarkContent
}
UIApplication.sharedApplication.setStatusBarStyle(statusBarStyle, animated = true)
onDispose { }
}
}
- A second example is when using Koin for dependency injection on specific platforms
When a platform needs some specificinstances
you might need to implement them on that platform
In this case, you can create anexpect
Koin module in commonMain
Here’s an example (we’ll cover how to use Koin in more detail later)
// in ~/commonMain/.../xxxx.kt
expect val platformModule: Module
fun appModule() =
listOf(platformModule)
// in ~/androidMain/.../xxx.kt
actual val platformModule: Module = module {
/** Datastore*/
single { dataStore(get<Context>()) }
/** Database*/
single<RoomDatabase.Builder<AppDatabase>> {
getAppDatabase(get())
}
}
// in ~/iosMain/.../xxx.kt
actual val platformModule: Module = module {
/** DataStore*/
single { dataStore() }
/** Database*/
single { getAppDatabase() }
}
The IDE provides a way
to help you more easily identify which platform a class
is implemented for
When you write the expect
keyword in shared code
if other platforms haven’t implemented the corresponding actual
The IDE will notify you that implementation is missing
At this point, you can simply hit the auto-generate button
and the IDE will create the missing implementation files
adding corresponding strings
to the file names for specific platforms
such as xxx.android.kt or xxx.ios.kt.
This way
you can more intuitively understand which platform each class belongs to
For example:
When you use command+f
to search for a function you just implemented
you can clearly know which platform’s class you’re currently looking at.