Jetpack Compose is a declarative framework UI developed by Google and dedicated for Android apps. It is an incredible improvement for Android ecosystem and give us the opportunity to create interfaces with a modern approach.
But Jetbrains are working on Compose too. They created Compose for Desktop, still in alpha version, and we can see first implementations for Compose for Web. It isn't impossible that Compose will be compatible with more platforms in the future, like iOS.
In this article, we'll see how we can build a kotlin multiplatform project (aka KMP) with Compose for Android and Desktop, and see how we can share components between them.
Modular structure
There are multiple strategies to share Compose components between applications. In our example, we'll create:
-
theme
module to define material theme, typographies, colors, shapes and icons. -
components
module to define all small reusable components and taketheme
module as dependency. -
android
module to create mobile screens fromcomponents
module and start an android app. -
desktop
module to create desktop screens fromcomponents
module and start a jvm app.
We won't share complete screens inside components
module. That guarantees us a similar user interface and experience for each platform depending their best practices.
Compose dependencies
Before starting to code, you need to know there are two kinds of dependencies: Compose by Google and Compose by Jetbrains. For both dependencies, artifact identifiers are the same and internal components are declared under androidx.compose
package.
But there is one main difference between these dependencies, Compose by Google is only compatible for Android apps whereas Compose by Jetbrains can be executed for Android and Desktop apps.
If you want to share components, you must use Compose by Jetbrains and use KMP to write specific code for each platform if necessary.
Creating multiplatform module
Inside a multiplatform module, you write your common source code shared between platforms but, sometimes, you may want to write specific implementation.
e.g. for android apps, you need Android context when you get a translation from strings.xml
files. To keep the native feature but to be compatible with others platforms, you need a mechanism to write specific code.
In our case, we write an application compatible with Android and Desktop. You'll find this file tree in multiplatform modules:
module
+- src
+- commonMain
+- androidMain
+- desktopMain
build.gradle.kts
This structure is possible due to the multiplatform gradle plugin where you can specify source set by platform.
build.gradle.kts
plugins {
id("com.android.library")
kotlin("multiplatform")
id("org.jetbrains.compose")
}
android {
// ...
}
kotlin {
android()
jvm("desktop")
sourceSets {
named("commonMain") {
dependencies {
// ...
}
}
named("androidMain") {
dependencies {
// ...
}
}
named("desktopMain") {
dependencies {
// ...
}
}
}
}
It is pretty simple to understand, we apply three plugins to define Android configuration if the module is executed for an Android app, multiplatform plugin to write common and specific source code and Compose desktop to use Jetpack Compose for Android and Desktop.
Note: you can only specify kotlin multiplatform maven dependencies in
commonMain
source set.
Writing custom implementation
Kotlin define a super simple and powerful way to write specific code for each platform. You just need to create an
expect
interface and give actual
implementations.
e.g. We want to load an image from an URL with Coil but this library is only compatible with Android because it requires Android Context. So, we need to define an expect composable function in our commonMain
source set and give an actual implementation in Android and Desktop source sets.
commonMain/src/RemoteImage.kt
@Composable
expect fun RemoteImage(
url: String,
contentDescription: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit
)
Android implementation is pretty simple because Accompanist library provide a Composable compatible with Coil. We just need to define the library in our androidMain
source set and call it in our actual composable function.
build.gradle.kts
kotlin {
sourceSets {
named("androidMain") {
dependencies {
implementation(Dependencies.Accompanist.coil)
}
}
}
}
androidMain/src/RemoteImage.kt
@Composable
actual fun RemoteImage(
url: String,
contentDescription: String?,
modifier: Modifier,
contentScale: ContentScale
) {
CoilImage(
data = url,
modifier = modifier,
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
)
}
Desktop implementation is a bit more complex because there is no equivalent to Coil in Desktop environment but Desktop is a jvm ecosystem, you can create your own implementation with OkHttp client. Just need more work on the composable. You can find a short implementation below but if you are interested in the whole implementation, check out my GitHub project used to illustrate this blog post.
build.gradle.kts
kotlin {
sourceSets {
named("desktopMain") {
dependencies {
implementation(Dependencies.okhttp)
}
}
}
}
desktopMain/src/RemoteImage.kt
@Composable
actual fun RemoteImage(
url: String,
contentDescription: String?,
modifier: Modifier,
contentScale: ContentScale
) {
val image = remember(url) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(url) {
ImageLoader.load(url)?.let {
image.value = makeFromEncoded(it).asImageBitmap()
}
}
if (image.value != null) {
Image(
bitmap = image.value!!,
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale
)
}
}
HTTP request is started only one time when we create our composable, custom helper class ImageLoader
is used to make the HTTP request and get the image binary and finally, the response is converted to an ImageBitmap
and used by a standard Composable function from Compose.
From now on, RemoteImage
can be used everywhere in our project and use the right implementation depending the platform execution from a single composable contract!
Conclusion
Kotlin Multiplatform is still in alpha but there are interesting perspectives and maybe, it could be a serious competitor for Flutter or React Native. If Compose become compatible with more platforms, like iOS apps, we can create full multiplatform apps with no compromise about performance!
If you want to see a real kotlin multiplatform project, you can check my open source GitHub project about movies and if you want to know more about me, you can follow me on Twitter @GerardPaligot.
Don't hesitate to chat with me! :)
Top comments (0)