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

Search for a command to run...

No comments yet. Be the first to comment.
I've been a professional software developer in the mobile space for around 15 years now. I started in the Android 2.1 / Eclipse / Ant days and iOS Objective-C days pre-ARC on Xcode 4 (if memory servic

Intro I recently learned about friendPaths as part of the Kotlin Gradle Plugin while browsing an Android Slack group. From the documentation on friendPaths: "Paths to the output directories of the friend modules whose internal declarations should be ...

If you've ever tried to compare the behavior of Row in Google's Jetpack Compose, HStack in Apple's SwiftUi, and something like display: flex in the web world, you'll see some interesting results. First, let's take a look at how HStack renders two lon...

Said another way, how to supercharge your development workflow with a debug drawer. As Android developers, we're constantly looking for ways to streamline our workflow. One often-overlooked tool that

As Jetpack Compose becomes more widely used across Android (and Multiplatform projects!), detecting regressions via tooling becomes more important to shift left and detect regressions earlier in the d

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.
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.
I hope these series of code snippets can prove helpful to someone implementing custom typography in their Compose Multiplatform app.