DEV Community

Gérard Paligot
Gérard Paligot

Posted on

Sharing Compose components between Android and Desktop

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 take theme module as dependency.
  • android module to create mobile screens from components module and start an android app.
  • desktop module to create desktop screens from components 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
Enter fullscreen mode Exit fullscreen mode

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 {
        // ...
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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! :)

Latest comments (0)