Hello. I'm an Android app developer on the Wadiz app development team.
Last July, Wadizdeveloped a benefits page. At that time, we introduced Server Driven UI to the 'Welcome Coupon' issuance screen. Server Driven UI is a mechanism that allows the client app to dynamically render screens when UI design information is provided from the server. This enables UI changes without redeploying the app.

Example JSON used in Server-Driven UI
Thanks to this, we can now operate the 'Welcome Coupon' page more conveniently and smoothly! However, writing and modifying JSON every time was a complex and difficult task. We needed to develop an admin tool so that anyone managing the page could easily handle the UI. We introduced and developed using Compose Multiplatform. Here's a brief overview of that experience.
Compose Multiplatform
Compose Multiplatform is a UI framework provided by JetBrains. It's a tool that allows developers to simultaneously build desktop and mobile apps using Kotlin and Compose, which are familiar to Android developers. While it includes some experimental features, it supports iOS, Android, Desktop, and Web.
To begin iOS app development, you'll need macOS, Java, Android Studio, Xcode, and CocoaPods. Install and run KDoctor to verify everything is set up correctly as shown below. Additionally, installing the Kotlin Multiplatform Mobile plugin is recommended.

Getting Started with Compose Multiplatform

IntelliJ IDEA
First, you'll need IntelliJ IDEA to get started. Select New Project → Compose Multiplatform, then enter the project name, save location, package name, and other details.
Configuration
- Single platform: Choose this when supporting a single platform—desktop or web—is sufficient.
- Multiple platform: Choose this when you need support for multiple platforms such as desktop, mobile, and web (Experimental).
Let's take a look at the project's packages.

The project is structured with a common module and platform-specific modules. The common module contains platform-independent code. Platform-dependent code, such as that for Android or desktop, is written in the respective platform modules.
common
commonMain: Platform-independent code
androidMain: Android actual function, class implementation
desktopMain: desktop actual function, class creation
android: Android platform-dependent code
AndroidManifest.xml
desktop: desktop platform-specific code
In this project, most of the code could be written in the commonMain package. This is because the green screen, built using Compose, is platform-independent.
However, certain features—such as image loading, setting file save paths, and displaying dialogs—differ across platforms due to variations in the libraries and environments used. To separate code according to platform and manage it efficiently, Compose Multiplatform utilizes the keywords expect and actual.

By specifying expect functions or classes in the common module using the same concept as Java interfaces, implementation functions or classes are written by attaching actual to each platform (Android, desktop). Adding libraries is also done identically by specifying them in build.gradle for each platform.

serialization-json Since the library is platform-independent, commonMainI added it. material3 The libraries require different implementations for Android and desktop platforms. Therefore, we added them separately for each platform. As a result, we can now efficiently manage the necessary libraries across multiple platforms.
Android Material 3
androidx.compose.material3:material3:1.0.1desktop material3
org.jetbrains.compose.material3:material3-desktop:1.3.0
Try writing some code
Next, I created a feature to add UI components to the screen. It displays a list. LazyRow Creates a Composable function to display the name and image for each option.

@Composable
private fun ComponentOptionItems(
clickAction: (ServerComponentType) -> Unit
) {
Spacer(modifier = Modifier.background(Color.Black).fillMaxWidth().height(1.dp))
LazyRow(
modifier = Modifier.fillMaxWidth()
.height(200.dp)
.background(Color.White),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
val typeList = ServerComponentType.values()
items(typeList) {
Column(
modifier = Modifier.fillMaxHeight().clickable {
clickAction.invoke(it)
},
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.padding(16.dp),
text = it.name,
color = Color.Black,
fontWeight = FontWeight.Bold
)
getPainterFile(it.name.lowercase())?.let { painter ->
Image(
painter = painter,
contentDescription = it.name
)
}
}
}
}
}
var openDialog: Pair<ServerComponentType, Component?>? by remember {
mutableStateOf(null)
}
showOptionSettingDialog(openDialog?.first, openDialog?.second, deleteAction = { ... })
ComponentOptionItems {
openDialog = Pair(it, null)
}openDialogcreate a variable named remember, ComponentOptionItems Changes the openDialog when the item is clicked. showOptionSettingDialogwhere openDialog It is being redrawn because the state has changed.
The method for displaying dialogs differs between Android and desktop. Therefore, we define an expect function and write an actual function tailored to each platform.
// common
@Composable
expect fun showOptionSettingDialog(
type: ServerComponentType?,
baseComponent: Component?,
deleteAction: () -> Unit, dismissAction: (Component?) -> Unit
)
// android
@Composable
actual fun showOptionSettingDialog(
type: ServerComponentType?,
baseComponent: Component?,
deleteAction: () -> Unit,
dismissAction: (Component?) -> Unit
) {
if (type != null) {
var component by remember { mutableStateOf(baseComponent) }
Dialog(
onDismissRequest = { dismissAction.invoke(null)},
properties = DialogProperties(usePlatformDefaultWidth = false),
content = {
... // 옵션 화면 그리기
}
)
}
}
// desktop
@Composable
actual fun showOptionSettingDialog(
type: ServerComponentType?,
baseComponent: Component?,
deleteAction: () -> Unit,
dismissAction: (Component?) -> Unit
) {
if (type != null) {
var component by remember { mutableStateOf(baseComponent) }
Dialog(
title = type.name,
visible = true,
state = DialogState(size = DpSize(1200.dp, 800.dp)),
onCloseRequest = {
dismissAction.invoke(null)
}) {
... // 옵션 화면 그리기
}
}
}At first glance, both Android and desktop call the Dialog composable function, but they are different functions.
// android
androidx.compose.ui:ui AndroidDialog.android
// desktop
org.jetbrains.compose.ui:ui-desktop Dialog.desktopThe option screens drawn in each dialog are identical across platforms. Therefore, we call the Composable function defined in common for shared use.

Desktop app

Android app
The results developed using Compose Multiplatform can run on both desktop and Android apps. You can also configure Server Driven UI screens to preview them. Extracting JSON data and saving it as a file is also possible.
Previously, it was a hassle to create JSON and test it on each platform. It was very convenient to be able to check the UI with the admin tool without that process.

In closing
Developing a desktop app seemed daunting at first, but I was also excited by the prospect of a new challenge. Being able to utilize Compose as well made me feel like I'd killed two birds with one stone after development.
However, there are still a few areas for improvement. Feedback suggested that a drawer navigation system, allowing modifications on a single screen rather than through dialogs, would be more convenient. We plan to improve the UI/UX based on this input.
Additionally, to make it easier for operations staff to use, we intend to replace sections written in development terminology like lineHeight, gravity, and margin with terms that are easier for anyone to understand.
I'm excited about the endless possibilities with Server Driven UI and Compose Multiplatform going forward. I'm also curious about the features that will be updated from the experimental features. It was a pleasure to gain such meaningful experience. 🙂🚀


