Library authors
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 withwithIos()need awithTvos()too, or everyexpectincommonMainreports a missingactualfor Native. - A custom intermediate source set wired with explicit
dependsOnper platform (Haze’sskikoMain) needs the same line fortvosMain. iosMaincode 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).