Adding tvOS to your own KMP library

This plugin was verified against two real, independent third-party Kotlin Multiplatform libraries — Koin’s koin-compose module and Coil’s coil-compose-core module — confirming resolution, compilation, and zero dev.sajidali leakage into non-tvOS targets in both cases. The recipe below is exactly what was applied.

1. Add the tvOS targets

In your library module’s build.gradle.kts:

kotlin {
    tvosArm64()
    tvosSimulatorArm64()
}

2. Apply the plugin in settings.gradle.kts, in the right order

Statement order matters. Get it wrong and Gradle rejects the script before the plugin ever loads:

// settings.gradle.kts

// 1. pluginManagement {} must be the first statement in the file.
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
        // whatever else your project already declares
    }
}

// 2. The plugins {} block applying the plugin comes after pluginManagement {}.
plugins {
    id("dev.sajidali.compose-tvos") version "1.4.2"
}

// 3. The composeTvos {} extension block comes after the plugins {} block that applies it.
composeTvos {
    verbose.set(true)
}

enableFeaturePreview(...), if you use it, can go on either side.

3. Check version lines before touching them

Don’t reflexively bump a dependency’s version to match the fork’s exact published line. Check whether the official artifact already ships a tvOS klib at the version your library already uses first — official-first resolution means the plugin will already resolve it correctly with no mapping needed, and bumping to a version that doesn’t exist upstream at all can break resolution for every platform, not just tvOS.

4. Verify resolution and compilation

./gradlew :your-module:dependencies --configuration tvosArm64CompileKlibraries
./gradlew :your-module:compileKotlinTvosArm64

The first command’s output should show dev.sajidali.* coordinates (or genuine official tvOS variants, for official-first cases) throughout your tvOS dependency graph, with no unresolved modules. The second should compile clean against those klibs.

As a control, confirm iOS (or another non-tvOS target) is completely unaffected:

./gradlew :your-module:dependencies --configuration iosArm64CompileKlibraries

This configuration’s output should contain zero dev.sajidali coordinates — the plugin only ever acts on tvOS-targeting configurations.

Since 1.4.0, this all also works for a symbol your commonMain (or another shared source set spanning tvOS and another platform) only reaches transitively — for example depending on foundation and referencing androidx.compose.ui.Modifier, a symbol that only ui actually declares. You do not need to add a direct dependency on the module that happens to declare the symbol just to make it visible on the tvOS side; if the equivalent single-target compileKotlinTvosArm64 compiles, the shared compileCommonMainKotlinMetadata compile should too. See Shared source sets and the visibility identity gate for the mechanism.

What consumers of your library then need

Applying this plugin in your own library’s settings.gradle.kts only affects your build: it lets your library’s own tvOS compilation succeed by redirecting your build’s dependency graph. It does not rewrite your library’s own published metadata — your published artifact’s .module file still declares its dependencies against the official org.jetbrains.* coordinates you wrote in dependencies {}.

That means anyone who depends on your library from their own tvOS Kotlin Multiplatform project still needs to apply dev.sajidali.compose-tvos in their own settings.gradle.kts too, so that their build’s dependency graph — which now transitively includes the same org.jetbrains.* Compose/AndroidX coordinates your library depends on — gets redirected the same way. A publish-time metadata rewrite (so a library could depend on dev.sajidali.* coordinates directly, with no consumer-side plugin needed) is a possible future direction but is not part of this plugin today.

Community-verified libraries

Third-party Compose Multiplatform libraries that have been built for tvosArm64 and tvosSimulatorArm64 with this plugin, from an unmodified upstream release tag, with no Kotlin source changes. “Verified” means the library’s main compilations pass and the iOS dependency graph is untouched; tests and runtime behaviour on Apple TV are not claimed unless stated. Each row links to a branch with the exact diff so the result is reproducible.

Library Version Modules Build change beyond plugin + targets Branch
Compottie 2.3.0 all seven library modules withTvos() in the three custom hierarchy groups tvos-compat
Haze 1.7.3 haze, haze-materials tvosMain { dependsOn(skikoMain) }; targets declared per module, not in the shared convention tvos-compat
Decompose 3.5.0 extensions-compose, extensions-compose-experimental (core already ships tvOS) Kotlin 2.1.0 to 2.3.20 and Compose 1.9.3 to 1.12.0 pins tvos-compat
Sketch 4.6.0 sketch-core, sketch-singleton, sketch-compose-core, sketch-compose withTvos() in the ios hierarchy group; Compose 1.11.1 to 1.12.0 pin tvos-compat
AboutLibraries 15.2.0 aboutlibraries-core, -compose, -compose-m2, -compose-m3 allWarningsAsErrors off for the tvOS compile tasks of compose-m3 (material3 deprecation) tvos-compat
Calf 0.13.0 calf-core, calf-ui (pickers, webview, permissions excluded) withTvos() in the material and nonAndroid groups, one 6-line tvosMain actual; tvOS takes the Material path since UIDatePicker/UISlider/UISwitch are unavailable on tvOS; Compose 1.11.1 to 1.12.0 tvos-compat
Circuit 0.38.0 backstack, circuit-runtime-ui, circuit-shared-elements, circuit-foundation, circuit-overlay, circuitx:effects, circuitx:navigation (five runtime modules already ship tvOS upstream) Compose 1.11.1 to 1.12.0 pins; two includeModule lines for the Plugin Portal filter tvos-compat
Landscapist 2.11.0 landscapist, coil3, landscapist-animation, landscapist-placeholder (palette excluded: kmpalette lacks tvOS) per-module applyHierarchyTemplate fragment for tvOS; Compose 1.11.1 to 1.12.0 and Skiko 0.150.1; one Kotlin change forced by 1.12.0 (asFrameworkPaint to skiaPaint) tvos-compat
Vico 3.3.1 vico:compose, compose-m2, compose-m3 iosMain renamed to appleMain (pure Kotlin actuals); settings-side versionMappings for stable material3 1.9.0; no pin bumps tvos-compat
kmpalette 4.0.0 kmpalette-core, androidx-palette, kmpalette-loader, extensions-base64, extensions-network (extensions-file excluded: filekit-core lacks tvOS) none; passed on the first attempt, no pin bumps tvos-compat
Reveal 5.2.0 reveal-common, reveal-core, reveal-shapes tvosMain { dependsOn(skikoMain) } in reveal-core; Compose 1.11.1 to 1.12.0 tvos-compat
ComposeCharts 1.0.0 compose-charts Kotlin 2.3.10 to 2.3.20 and Compose 1.10.1 to 1.12.0 pins (declared iosX64 stops resolving under 1.12.0, disclosed) tvos-compat
MaterialKolor 5.0.1 material-kolor, material-color-utilities none; passed on the first attempt, no pin bumps (7-line diff) tvos-compat
Telephoto 0.19.0 zoomable, annotations, zoomable-image:sub-sampling-image (image-loader modules are Android-only) iosMain platform actual moved to appleMain; Kotlin 2.1.21 to 2.3.20, Compose 1.8.0 to 1.12.0, Poko 0.22.0, Gradle 9.1, language version 1.9 to 2.1 (K2), context receivers to context parameters tvos-compat
compose-shimmer 1.5.0 shimmer withTvos() in the module’s skiko group; Compose 1.11.0 to 1.12.0 tvos-compat
ZoomImage 1.6.0 zoomimage-core, -compose, -core-coil3, -compose-coil3-core, -compose-coil3, -compose-resources (sketch4 modules excluded: published Sketch lacks tvOS) opt-in tvos flag in buildSrc, withTvos() in the ios group; Compose 1.11.1 to 1.12.0 tvos-compat
constraintlayout-compose-multiplatform 0.8.2 compose, compose-shaded, compose-shaded-compose targets in the build-logic convention (applied only by the three library modules); Compose 1.11.1 to 1.12.0 tvos-compat
qrose 1.1.2 qrose, qrose-core, qrose-oned withTvos() in the skiko and jvmNative groups; Kotlin 2.3.0 to 2.3.20, Compose 1.10.0 to 1.12.0 (declared iosX64/macosX64 stop resolving under 1.12.0, disclosed) tvos-compat
compose-sonner 0.4.0 sonner iosMain platform actual moved to appleMain; Compose 1.11.1 to 1.12.0 tvos-compat
Koalaplot 0.12.1 koalaplot-core iosMain renamed to appleMain with explicit tvOS dependsOn (the project disables the default hierarchy template); Compose 1.11.1 to 1.12.0 plus a versionMappings entry for material3 1.9.0 tvos-compat
compose-icons 2.2.1 all 32 icons-*-cmp packs (3 compiled as evidence) Kotlin 2.2.21 to 2.3.20 and Compose 1.9.3 to 1.12.0; the Kotlin bump forces a kotlinOptions to compilerOptions migration across the Android modules; builds need --configure-on-demand tvos-compat
Compose-Settings 3.2.0 ui-core, ui-tiles, ui-tiles-extended, ui-tiles-expressive none; passed on the first attempt, no pin bumps, two versionMappings entries for the material3 lines tvos-compat
FileKit 0.15.0 filekit-core, filekit-coil (dialogs excluded: document/photo/camera pickers do not exist on tvOS) a 64-line tvosMain mirroring the library’s own watchosMain stubs for the Photos-backed gallery functions; Compose 1.11.1 to 1.12.0 tvos-compat
Voyager 2.2.21-1.10.3 all eight KMP modules (kodein excluded: no tvOS klib for kodein-di-framework-compose) Kotlin 2.2.21 to 2.3.20, Compose 1.10.3 to 1.12.0, lifecycle 2.11.0, koin-compose 4.2.2; iOS/macOS BackHandler no-op actuals merged into appleMain tvos-compat

Shared source sets resolve correctly since plugin 1.4.0. A symbol your commonMain reaches only transitively (depending on foundation and referencing androidx.compose.ui.Modifier, say) compiles in the shared metadata compilation as well as the per-target ones, with no need to declare a direct dependency on whichever module happens to declare the symbol.

These are compile proofs, not published artifacts. A tvOS app cannot consume the upstream coordinate of any of these libraries until either the maintainer publishes tvOS variants or a tvOS build is republished under a group the plugin redirects (as is done for Koin and Coil). See What consumers of your library then need.

Recurring patterns from these builds, useful if you are adding tvOS to your own library:

  • Custom hierarchy groups (skiko, jvmNative, ios) declared with withIos() need a withTvos() too, or every expect in commonMain reports a missing actual for Native.
  • A custom intermediate source set wired with explicit dependsOn per platform (Haze’s skikoMain) needs the same line for tvosMain.
  • iosMain code that only touches UIKit, Foundation, CoreGraphics or Network compiles unchanged for tvOS. Whether it behaves correctly on Apple TV is a separate question.
  • Libraries pinned below Kotlin 2.3.20 must bump Kotlin first (see Requirements).