Compose Multiplatform with shared resources
Kotlin Multiplatform, together with Compose Multiplatform, is becoming a serious competition for multiplatform frameworks like Flutter or React Native. Besides business logic and user interfaces, there is another aspect that should be shared across platforms: resources.
This blogpost continues Migrating to Kotlin Multiplatform and its chapter about resources
Table of Contents
Plugins
As of April 2024 we have no definite answer to shared resources with Compose Multiplatform. Luckily there are already multiple plugins available that fulfill this requirement to a varying degree. We will dive into the most promising two.
Compose Multiplatform resources
JetBrains added multiplatform resources to its roadmap and its documentation in November 2023. In February 2024 they released a first experimental version of compose.components.resources
with Compose Multiplatform 1.6.0. One month later “Resources” occupy a good chunk of the changelog for Compose Multiplatform 1.6.1.
Compose Multiplatform resources works in a similar way to Android’s native App resources. Resources accessors are generated during compilation (for everything within composeResources
) and available through one single source of truth (Res.kt
).
shared
build/generated/compose/resourceGenerator/kotlin/<rootProject>/shared/generated/resources
Res.kt
src/commonMain/composeResources
drawable
files
font
values
Res.kt
itself is an object
providing accessors for every resource type.
internal object Res {
public object drawable
public suspend fun readBytes(path: String): ByteArray = readResourceBytes(path)
public object font
public object string
}
Compose Multiplatform resources provides representations for some resource types, e.g. StringResource
or DrawableResource
. They ensure type-safety and can be consumed within a Composable context:
@Composable
fun painterResource(resource: DrawableResource): Painter
@Composable
fun Font(resource: FontResource): Font
@Composable
fun stringResource(resource: StringResource): String
MOKO resources
MOKO resources works similarly to Compose Multiplatform resources but started development much earlier in 2019. It is one of many components by IceRock Development targeting Kotlin Multiplatform. Therefore its name: MObile KOtlin project. This project is well-documented, has more than thirty contributors and almost 1k stars on GitHub.
Unfortunately it has become somewhat stale, as issues are responded to sparsely and the last minor update 0.23.0
was almost a year ago. 2024 started promising with weekly alpha versions but that trend ended two months later. Personally I experienced one crash after upgrading the Android Gradle Plugin to 8.3.0 (see #652) and the issue tracker is approaching a point where open issues outnumber closed ones.
Comparison
Currently Compose Multiplatform resources is leagues behind MOKO resources. While the former does not support multi-module projects or splitting up resources into multiple files yet, the latter’s featureset is extensive and its configuration customizable.
Both solutions support the usual resource types and common restrictions boil down to the lowest common denominator of every supported platform. Some platforms may not support certain types, like .dimens
or .mipmap
, or use proprietary ones, like .xml
or .mlmodel
. color
may be missing but can be covered by the app’s theme within the Composable realm.
Resource type | Compose Multiplatform resources | MOKO resources |
---|---|---|
String | ✅ (except plurals) | ✅ |
Image (Rasterized) | ✅ (.png, .jpg, .bmp, .webp) | ✅ (.png, .jpg) |
Image (Vectorized) | ✅ (.xml) | ✅ (.svg) |
Font | ✅ (.ttf, .otf) | ✅ (.ttf, .otf) |
File | ✅ (raw) | ✅ (raw, assets) |
Color | - | ✅ |
Conclusion
As of April 2024 neither of the two solutions is production-ready. If splitting up resources is not an option, MOKO resources should be the best bet, but expect breaking changes and open issues along the way. If both projects keep their current pace, I expect Compose Multiplatform resources to surpass MOKO resources in the near future.
Tutorial
The following tutorial has been written during my rewrite of Diaguard, a previously ten-years-old native app written in Java.
Migration from MOKO resources to Compose Multiplatform resources
After one year with MOKO resources I decided to migrate to Compose Multiplatform resources. I tried to abstract everything domain-specific, so this tutorial can be applied to similar projects. I did not use any custom fonts, but migrating those and other types should work similarly to strings and images.
Prerequisites
Dependencies for both MOKO resources and Compose Multiplatform resources have been implemented and are working. The project is single-module.
Versions: MOKO resources 0.24.0-alpha-5, Compose Multiplatform resources 1.6.1
Move resources
Move resource folders:
- src/commonMain/moko-resources
+ src/commonMain/composeResources
Rename folders for resource types:
- base
+ values
- <locale>
+ values-<locale>
- images
+ drawable
Convert resources
Convert string templates from implicitly- to explicitly formatted, e.g. for strings placeholders:
- %s Hello, World %d
+ %1$s Hello, World %2$d
Convert vector drawables from .svg
to .xml
via Android Studio’s Resource Manager
- Resource Manager -> Drawable -> + -> Import Drawables
- Select everything
.svg
that should be converted to.xml
- Move the generated
.xml
tosrc/commonMain/composeResources/drawable
Migrate imports
Next we need to migrate all imports for the resource linkers that are bridging Kotlin to XML. Android developers know this bridge as R.java
which will be generated during compilation in the build folder. MOKO resource calls it MR
(by default) and places it at <namespace>.MR
. Compose Multiplatform resources calls it Res
and places it at app.shared.generated.resources.Res
.
This migration steps makes excessive use of Edit/Find/Replace in Files…
to avoid touching every file that uses resources.
Tip: Disable
Add unambiguous imports on the fly
andOptimize imports on the fly
forKotlin
inFile/Settings/Editor/General/Auto Import
. This will keep Android Studio from scrambling imports up in the process and can be re-enabled after this chapter.
- import <namespace>.MR
+ import <rootProject>.shared.generated.resources.*
- import dev.icerock.moko.resources.compose.stringResource
+ import org.jetbrains.compose.resources.stringResource
- import dev.icerock.moko.resources.compose.painterResource
+ import org.jetbrains.compose.resources.painterResource
- import dev.icerock.moko.resources.ImageResource
+ import org.jetbrains.compose.resources.DrawableResource
The wildcard speeds up the transition of imports which would have otherwise be declared individually for every resource. If you want to avoid wildcards, optimize your imports afterwards via Android Studio’s Code / Optimize imports
.
Migrate function calls
Strings and images can be migrated project-wide, since MOKO resources and Compose Multiplatform resources share a similar API:
- MR.strings
+ Res.string
- MR.images
+ Res.drawable
Files do not yet support resource linking, so every call-site requires manual migration:
- MR.files.<filename>.readTextAsState()
+ runBlocking { Res.readBytes("files/<filename>.<filetype>").decodeToString() }
Let me break down those changes:
- Files are read asynchronously, so we use
runBlocking
for call-sites that operate synchronously - Files are referenced by file name including -type and by their relative path from
src/commonMain/composeResources
- Files are returned as
ByteArray
, so we decode them, e.g. toString
viaByteArray.decodeToString(): String
Encapsulation
Kotlin Multiplatform and especially Compose Multiplatform are still moving targets. Encapsulation can significantly reduce the migration effort. The fewer locations that access third-party libraries, the less effort is required to replace them. Since resources are such an essential part of every user-facing application, effort and return must be weighed up.
In my case, I applied all described steps to migrate my small app within a few hours. Then I tried to encapsulate the most redundant code, like file access, but leaving access to the resource linker scattered across the project. This will be my sweet spot between simplifying another potential migration process and hoping that Compose Multiplatform resources is here to stay.
Time will tell.
Faltenreich/diaguard/tree/feature/multiplatform