Ketoy

← SDK updates

Alpha

Custom fonts: FontFamily(Font(R.font.X))

May 19, 2026 · SDK · 0.3.4-alpha

Drop your .ttf / .otf into res/font/, register it once at app startup, then reference it from KBC source exactly the way you would in any other Compose codebase.

In your KBC source

HeroCard.kt
@KetoyComposable
fun HeroCard() {
  Text(
      text = "Ship Kotlin. Server-side. Today.",
      fontSize = 18.sp,
      fontFamily = FontFamily(Font(R.font.courgette_regular)),
  )
}

That's the entire surface area. The compiler plugin notices the FontFamily(Font(R.font.X)) shape and atomically collapses it into a single string literal carrying just "courgette_regular". At render time, your host APK resolves that name to the real FontFamily you registered, no reflection, no getIdentifier lookups, no risk of R8 silently stripping the resource in release builds.

The five generic family singletons (FontFamily.Default, FontFamily.SansSerif, FontFamily.Serif, FontFamily.Monospace, FontFamily.Cursive) work too, with zero host-side setup.

Wiring it on the host, with Hilt

KetoyModule.kt
@Provides
@Singleton
fun provideMaterialFontFamilyResolver(): MaterialFontFamilyResolver =
  materialFontFamilyResolver {
      register(
          "courgette_regular",
          FontFamily(Font(R.font.courgette_regular)),
      )
  }

@Provides
@Singleton
fun provideKetoyConfigCustomizer(
  fontFamilyResolver: MaterialFontFamilyResolver,
): KetoyConfigCustomizer = KetoyConfigCustomizer { default ->
  default.copy(fontFamilyResolver = fontFamilyResolver)
}

Every KBC screen rendered through KetoyScreen picks up the resolver automatically.

Wiring it on the host, without Hilt

MyApplication.kt
class MyApplication : Application() {
  lateinit var ketoyRuntime: KetoyRuntime

  override fun onCreate() {
      super.onCreate()

      val fontResolver = materialFontFamilyResolver {
          register(
              "courgette_regular",
              FontFamily(Font(R.font.courgette_regular)),
          )
      }

      val config = KetoyConfig(fontFamilyResolver = fontResolver)

      ketoyRuntime = KetoyRuntime(
          capabilityRegistry = CapabilityRegistry().apply {
              registerCoreCapabilities(context = this@MyApplication)
          },
          config = config,
      )
  }
}

The R.font.X reference at the register(...) call site is a compile-time field read, R8 sees it and keeps your font resource alive in release builds. No -keep rules required.

Multi-font families

Typography.kt
val brand = FontFamily(
  Font(R.font.inter_regular, FontWeight.Normal),
  Font(R.font.inter_medium,  FontWeight.Medium),
  Font(R.font.inter_bold,    FontWeight.Bold),
)
materialFontFamilyResolver {
  register("inter_regular", brand)
  register("inter_medium",  brand)
  register("inter_bold",    brand)
}

KBC source can reference any of the three resource names and get the full family, Compose picks the right weight at render time.

FontFamily()Font()R8-safeHilt + non-Hilt