Simple way to use common resources in Kotlin multi-platform project

Alex Maryin
5 min readNov 13, 2021
Photo by Lysander Yuen on Unsplash

When I was started with studying Kotlin multi-platform programming and Compose I was so impressed and wondered, because my previous experience with declarative UI-frameworks wasn’t so clear and smooth. And one of the biggest matter was diving into Android UI-world with xml-mazes and nightmares of State.

With Kotlin multi-platform style we are able to write common code for all of choosen targets and not only business-logic or amazing algorithms, but user interface too and with modern declarative approach.

Of course, there are dozen of things that can’t be subject to unify due to difference of operational systems and hardware. One of these are the files with resources, assets, etc. We should provide a separate code for each platform to read these files.

One approach is to use a third-party library which already done it for us. Maybe you know one of the production-ready library called Moko-resources, and it is a part of the bigger set of libraries specially developed for Kotlin multi-platform.

The other way is to build your own bicycle. On the one hand, it will help us to understand how that libraries works, and on the other hand, it get us to avoid redundant dependencies in the simplest cases.

In generally, Kotlin provides a feature that allows to invoke a function from common code which implementation is written in a platform-specific code. This function must have “expect” keyword in declaration place and implementation part must have “actual” keyword. Both of the declared and implemented function must be placed in the same package (in fact the files may be placed in different folders in the project, but package name at the header should be the same).

Let’s start with Gradle, because we need to define a common folder in the project which would be used as resources source for our platforms. My targets are Android and Desktop, so I should define its in the build.gradle.kts file of the Common module:

kotlin {
android()
jvm("desktop")

sourceSets
{
named("commonMain") {
resources.srcDirs("resources")
// other parts

}
}
}

android {

sourceSets {
named("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml"
res.srcDirs("resources")
}
}
}

Now I shall put all of my resources into the project_root/common/resources/ folder. Due to Android specific it’s important to save the folders structure inside for different resources as defined: folder drawable for pictures and photos, folder values for strings, colors and other xml-files, folder raw for different assets, for example json-files, etc. Further I’ll explain this approach with my project Sims checklists. It’s the open-source project specially written for studying multi-platform Compose.

Loading JSON from raw resource

At the first, I need main JSON file for the project which contains all data. Suppose, it is the simplest kind of the local database. Put it in resources/raw/aircraft.json. And now I declare at commonMain module that it’s expecting a function for loading json:

const val AIRCRAFT_FILE = "aircraft.json"

expect fun loadAircraftJson(filename: String): String

Its enough to use declared function in any place of the commonMain module as regular function. Let’s implement a body in androidMain and desktopMain modules. It is a very simple to do for Desktop because there are a dozen of Compose functions for manipulating with resources in jvm:

actual fun loadAircraftJson(filename: String): String = 
useResource("raw/$filename") { it.bufferedReader().readText() }

It will be a little bit harder for Android. Remember that Andorid uses a Context-abstraction for maintaining application lifecycle in the system. But we can’t get the context outside Android Class which implement this interface, i.e. Activity, Fragment, Service or Application. Thanks to Kotlin multi-platform we can implement the Application inside the androidCommon module (we can imlement any other Class, for example Activity, but as we use Compose we don’t need it anymore). I don’t use DI-framework due to simplicity of the project, so I apply Singleton-pattern to get the Application instance from any other place:

class AppAndroid : Application() {

override fun onCreate() {
super.onCreate()
instance = this
}

companion object{
private lateinit var instance: AppAndroid
fun instance() = instance
}
}

Using our AppAndroid.instance() function we are getting access to its properties as resources, packageName, assets, configuration, theme and many other.

And now it’s time to implement Android-part of the expected function:

actual fun loadAircraftJson(filename: String) =
with(AppAndroid.instance()) {
val resourceId = resources.getIdentifier(
filename.substringBefore("."), "raw", packageName
)
resources
.openRawResource(resourceId)
.bufferedReader().readText()
}

We can use direct links to our resources, of course, i.e. R.raw.aircraft, from the build-time generated class R with application resourses ids, strings, values and other. But I used getIdentifier function to show how to get resource by the name only.

That’s all and it works from either Android and Desktop, I’ve checked it.

Loading vector image from XML-resource

To load a vector image written in xml resource we only need to do the same two steps: to define expect function at commonMain and to implement actual function at androidMain/desktopMain module.

expect fun loadXmlPicture(name: String): ImageVector

Important point that the name should be as it allowed in Android — without file extension. So, here are two actual implements, one for Android part:

@Composable
actual fun loadXmlPicture(name: String): ImageVector = with(AppAndroid.instance()) {
ImageVector.vectorResource(resources.getIdentifier(name, "drawable", packageName))
}

And other for Destop part:

@Composable
actual fun loadXmlPicture(name: String): ImageVector =
useResource("drawable/$name.xml") { stream ->
loadXmlImageVector(InputSource(stream), LocalDensity.current)
}

I used @Composable annotation because extensions vectorResource, useResource and loadXmlImageVector are the part of Compose library. It’s not necessary to annotate expect function with it.

Loading bitmap image from JPG file

For this point I suggest to use async loading of file because some of them may be large and it’s loading will affect main thread. At first, we can define a generic function in common code for this purpose:

@Composable
fun <T> AsyncImage(
loader: suspend () -> T,
painterFor: @Composable (T) -> Painter,
contentDescription: String,
modifier: Modifier = Modifier
) {
val image: T? by produceState<T?>(null) {
value = loader()
}
image?.let {
Image(
painter = painterFor(it),
contentDescription = contentDescription,
modifier = modifier
)
}
}

Extension produceState creates the coroutine which being loaded the image from file, then loaded image will trigger its recomposition, because this function is composable, and Image will be drawn. This approach I found at official Jetbrains Compose example.

Then, define expect function for loading from resources:

expect suspend fun loadAircraftJpgPhoto(name: String): Painter

In the UI part of the commonMain module we use AsyncImage as one of UI-elements from Compose:

AsyncImage(
loader = { loadAircraftJpgPhoto(item.photo) },
painterFor = { it },
contentDescription = "Photo of ${item.name}",
modifier = Modifier
.padding(8.dp)
.size(150.dp)
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(16.dp),
clip = true
)
)

It only remains to implement actual functions for platforms. The one for Android:

actual suspend fun loadAircraftJpgPhoto(name: String): Painter =
with(AppAndroid.instance()) {
withContext(Dispatchers.IO) {
val drawableId = resources.getIdentifier(name, "drawable", packageName)
BitmapPainter(resources.getDrawable(drawableId, theme).toBitmap().asImageBitmap())
}
}

And other for desktop:

actual suspend fun loadAircraftJpgPhoto(name: String): Painter =
withContext(Dispatchers.IO) {
useResource("drawable/$name.jpg") { stream ->
BitmapPainter(loadImageBitmap(stream))
}
}

As you can see platform-dependent code uses a similar approach to get resources. Difference is concerning only methods to get access to files, Android has own principes and aims to achieve compile-safety approach and Desktop use the simple way to use resources as regular files.

You can define other functions to get string resources, color resources and other what you need in your multi-platform application as easy as drawable or raw resources described here.

Thank you for reading this and don’t hesistance to post any advices and comments for me.

--

--

Alex Maryin

My profession is lawyer, but I like programming for a whole my life. It started since 90's with ZX-spectrum, and now I fan of Kotlin, Python, Pascal & C++.