Seamless Typography in Action: Implementing Custom Fonts in Compose Multiplatform for Android, iOS, and Desktop

Hey! I’m Joe! I’m an Android developer who is passionate about the human side of software development.
I recently needed to use a custom font for my Compose multiplatform project Sync Sphere and I thought I'd share my solution in case it was helpful to anyone else.
First, we need to add a font to commonMain/resources/font

For this example, I have gone with dancingscript_regular.ttf The resource name should follow android resource conventions and be all lowercase with underscores if necessary.
I am adding this font to part of an app theme, but this part is optional.
@Composable
fun AppTheme(
content:
@Composable()
() -> Unit,
) {
MaterialTheme(
content = content,
typography = appTypography,
)
}
appTypography is defined as a variable that creates a custom typography.
val appTypography : Typography
@Composable
get() {
return Typography(
defaultFontFamily = dancingScriptRegular
)
}
dancingScriptRegular is a custom font that we use expect/actual to get a different platform implementation.
val dancingScriptRegular: FontFamily
@Composable
get() {
return FontFamily(
font(
"DancingScript",
"dancingscript_regular",
FontWeight.Normal,
FontStyle.Normal,
),
)
}
The font function is the main bread and butter of our solution here. This is where we use expect/actual to give Android / iOS / Desktop implementations.

Under shared/src/commonMain/kotlin, I created a FontResource.kt class that looks like
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
expect fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font
Under androidMain, iosMain, and desktopMain we will define our actual implemetations.

androidMain:
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
@Composable
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)
}
iosMain:
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 kotlinx.coroutines.runBlocking
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.resource
private val cache: MutableMap<String, Font> = mutableMapOf()
@OptIn(ExperimentalResourceApi::class)
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
// use a cache to store fonts and re-use
return cache.getOrPut(res) {
val byteArray = runBlocking {
resource("font/$res.ttf").readBytes()
}
androidx.compose.ui.text.platform.Font(res, byteArray, weight, style)
}
}
desktopMain:
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
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
androidx.compose.ui.text.platform.Font("font/$res.ttf", weight, style)
Lastly, in your shared module build.gradle.kts under the android block, you'll have to add this line so that the Android app will know to use commonMain/resources and a res src directory.
android {
compileSdk = (findProperty("android.compileSdk") as String).toInt()
namespace = "com.myapplication.common"
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
// This is the line to add
sourceSets["main"].res.srcDirs("src/commonMain/resources") // <=== This is the line to add
// This is the line to add
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
defaultConfig {
minSdk = (findProperty("android.minSdk") as String).toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
jvmToolchain(17)
}
}
And then you should be able to run your project on iOS, Android, and Desktop using a common font.



CocoaPods
A finishing note about CocoaPods. When I was writing this article, I was using the most up-to-date version of the Compose Multiplatform template on Github from JetBrains as a demo. When I was adding this to my project Sync Sphere, I had to do some additional work to get fonts working on iOS. After doing all of the steps listed above, I also had to run ./gradlew :shared:podInstall to get the fonts copied over to iOS, otherwise I was running into a resource not found exception.
Conclusion
I hope these series of code snippets can prove helpful to someone implementing custom typography in their Compose Multiplatform app.




