From Android to Multiplatform: Real 100% Jetpack Compose App. Part 1 — Resources

Marko Novakovic
4 min readMay 7, 2023

--

Photo by Markus Winkler on Unsplash

In this series, I’ll walk you through the steps to migrate an Android app to a 100% Jetpack Compose multiplatform app. The first step is to migrate the resources, including strings, images, icons, and fonts. While this won’t have an immediate or visual impact on the iOS app, it sets the foundation for the future.

I could create and maintain separate, but same, resources across platforms but that’s not optimal. Plus I’m striving for 100% Kotlin app. I’ve used moko-resources to share resources and have all of them inside shared module.

I had to migrate:

  1. Strings
  2. Drawables
  3. Fonts

Before actual migration there is moko-resources setup to be done.

Setup

  1. Inside root build.gradle.kts:
buildscript {
repositories {
gradlePluginPortal()
}

dependencies {
classpath "dev.icerock.moko:resources-generator:0.22.0"
}
}


allprojects {
repositories {
mavenCentral()
}
}

2. Inside shared build.gradle.kts pluggin needs to be applied:

plugins {
...
id("dev.icerock.mobile.multiplatform-resources")
}

After Gradle Sync we get the option to use multiplatformResources block to set up moko-resources:

multiplatformResources {
multiplatformResourcesPackage = "com.your.shared.package"
}

Here we have the option to set more things up like: resources class name, resources class visibility, if you’re common code module is not named commonMain you have to set multiplatformResourcesSourceSet = “commonModuleName”

3. Add dependencies to top level of shared build.gradle.kts :

plugins {
...
}

kotlin {
...
}

multiplatformResources {
multiplatformResourcesPackage = "com.your.shared.package"
}

// important part
dependencies {
commonMainApi("dev.icerock.moko:resources:0.22.0")
commonMainApi("dev.icerock.moko:resources-compose:0.22.0") // for compose multiplatform

commonTestImplementation("dev.icerock.moko:resources-test:0.22.0") // for testing
}

sqldelight {
...
}

android {
...
}

I didn’t write any tests for moko-resources nor tests that utilise them so I didn’t try test one. Honestly, I don’t know exactly what it is for.

4. Point platform to use shared resources:

Inside android block from shared build.gradle.kts:

android {
...
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
}

iOS:

kotlin {
android()
iosX64()
iosArm64()
iosSimulatorArm64()

cocoapods {
version = "1.0.0"
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
ios.deploymentTarget = "14.1"
podfile = project.file("../iosApp/Podfile")
framework {
baseName = "shared"
}
// this is important part
extraSpecAttributes["resources"] =
"['src/commonMain/resources/**', 'src/iosMain/resources/**']"
// if you don't have anything inside iosMain/resources than you can have:
// extraSpecAttributes["resources"] = "['src/commonMain/resources/**']"
// I left it in just as a reminder that it's possible and how to do it
}

...
...
...
}

5. And the last general setup step is to create resources directory.

Inside shared/src/commonMain create resources directory.

Gradle sync…

Strings

This is pretty easy part. I already had strings.xml for my Android app. I just copy/pasted existing strings.xml file to commonMain/resources/MR/base and. If you have strings for different locales than paste string.xml for each locale to commonMain/resources/MR/<locale_code>, I didn’t.

That’s it. Gradle sync will create MR.strings that will be used later. If sync doesn’t do the job than manually run generateMRcommonMain Gradle task.

At this point I’m not using strings anywhere because I don’t have any UI components migrated but if you can’t wait for upcoming post this is how to use shared strings:

Text(text = dev.icerock.moko.resources.compose.stringResource(MR.strings.string_res))
// MR is generated by Gradle Sync or by running generateMRcommonMain task manually

Drawables

moko-resources has a capability to share drawables but I didn’t use it because Compose Multiplatfrom offers out of the box solutions. It’s experimental in the time of writing this article.

Inside shared.build.gradle.kts commonMain source set dependencies add:

@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)

Same as for strings, I had drawables folder for the Android app so I just copy/pasted drawables, whole folder, to shared/commonMain/resources.

Again, I don’t have anything to use drawables for but this is how you would use them:

Icon(painter = org.jetbrains.compose.resources.painterResource("drawable/some_drawable.xml"))

Fonts

Same as for drawables, I ended up not using moko-resources. It has an option to do so but it was hard to make it work with existing Android Compose theme. There needed to be a lot of changes and refactorings that I wasn’t willing to do.

Instead I took advantage of expect/actual mechanism which allowed me to basically copy/paste Android Compose theme.

I copy/pasted fonts from existing Android app to commonMain/resources/font.

Onto expect/actual. Declaring expect fun inside commonMain:

import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight

@Composable
internal expect fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font

actual implementations.

Android:

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight

@SuppressLint("DiscouragedApi")
@Composable
internal actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
val context = LocalContext.current
val id = context.resources.getIdentifier(res, "font", context.packageName)
return Font(id, weight, style)
}

iOS:

import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font
import kotlinx.coroutines.runBlocking
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.resource

private val cache: MutableMap<String, Font> = mutableMapOf()

@Composable
internal actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
return cache.getOrPut(res) {
val byteArray = runBlocking {
try {
resource("$res.ttf").readBytes()
} catch (e: Exception) {
resource("$res.otf").readBytes()
}
}
Font(res, byteArray, weight, style)
}
}

That’s it for this blog post. I hope it was helpful and/or you learned something new.

It wasn’t action packed post but it shares important step that I struggled to implement.

Next post will be about migrating theme, which will be pretty straight forward and that’s where real fun begins: migrating UI components and screens.

--

--