Mount your SwiftTUI app inside a Jetpack Compose UI: the same App, @State, and @FocusState you run in a terminal, now a native Compose view.
swift-tui-android is the Android host for SwiftTUI. The
Gradle plugin cross-builds your Swift-authored view tree to a native .so.
The SwiftTUIHostView composable renders it. The same View tree can run in a
terminal, a WASI bundle, a local WebHost, a native SwiftUI surface, or Android.
See it on a device: the counter demo's
AndroidExample
consumes these exact artifacts — plugin 0.9.11 and
sh.swifttui:android-host:0.9.11 — and runs on an emulator or phone. It hosts
the same CounterView that the terminal, SwiftUI, and browser hosts run, so it
shows the one-source-many-hosts claim rather than describing it.
Pre-1.0 (0.9.11 beta). Published via GitHub Pages until the Gradle Plugin Portal / Maven Central graduation.
Android is a 0.9 preview, arm64-only support tier:
arm64-v8a, API 28+, NDK27.3.13750724, Swift 6.3.x, and the Swift Android SDK through the published AAR/Gradle-plugin packaging path. The lower-level host can cross-compile x86_64, but x86_64 packaging and IME marked/pre-edit composition are outside the 0.9 claim. The Compose accessibility overlay is one-way semantic presentation; TalkBack-origin focus and control actions do not route back into SwiftTUI.
- One codebase, every surface. Author your UI once against SwiftTUI and host it on Android next to the terminal, WASI, WebHost, and Apple-platform hosts, so a new platform costs you a host module, not a rewrite.
- Drop-in Compose.
SwiftTUIHostViewis an ordinary composable. Mount it with onesetContent { … }call, with noAndroidViewinterop glue to write. - The build is handled for you. The Gradle plugin cross-compiles your Swift
host product for
arm64-v8a, renames the output canonically, and stages it plus the Swift runtime for your APK'sjniLibs. You declare the source set and the SwiftPM product; the plugin does the toolchain plumbing. - Lean APK. The host AAR is a Compose view plus a JNI shim and does not bundle the Swift runtime; the plugin supplies that from your Swift Android SDK, so your APK ships exactly one copy.
| Artifact | Coordinates | What it is |
|---|---|---|
| Host library (AAR) | sh.swifttui:android-host |
A Compose SwiftTUIHostView + the JNI shim that bridges to the Swift SwiftTUIAndroidHost C ABI. Runtime not bundled. |
| Gradle plugin | sh.swifttui.android |
Cross-builds your Swift host product to an Android .so and copies it + the Swift runtime (from your Swift Android SDK) into your app's jniLibs. |
It is the Android sibling of
swift-tui-swiftui (native
macOS/iOS host) and swift-tui-web
(browser host). The runtime it drives lives in
swift-tui.
Four files. The layout below is the plugin's default; see Where the Swift package goes to put it elsewhere.
MyApp/
├── settings.gradle.kts
├── app/
│ ├── build.gradle.kts
│ └── src/main/kotlin/…/MainActivity.kt
└── SwiftPackage/ ← sibling of the app module
├── Package.swift
└── Sources/MyAppHost/MyApp.swift
The counter demo's
AndroidExample
is this layout filled in — plus the ordinary Android scaffolding every app has
(root build.gradle.kts, gradle.properties, a manifest, the wrapper). Read it
alongside the four steps below if you would rather start from a working copy
than from a blank directory.
1. settings.gradle.kts — register the Pages repo for BOTH the plugin and
the AAR (until the Plugin Portal / Maven Central graduation). pluginManagement
must come first and is evaluated on its own, so the URL is repeated rather than
shared through a val:
pluginManagement {
repositories {
gradlePluginPortal(); google(); mavenCentral()
maven { url = uri("https://swifttui.github.io/swift-tui-android") }
}
}
dependencyResolutionManagement {
repositories {
google(); mavenCentral()
maven { url = uri("https://swifttui.github.io/swift-tui-android") }
}
}
rootProject.name = "MyApp"
include(":app")2. app/build.gradle.kts. The plugin stages the Swift host library and
runtime into build/generated/swiftJniLibs, but packaging them is your app's
decision — so the jniLibs source set and useLegacyPackaging below are
required, not optional. Leave the jniLibs source set out and the APK would
still build, install, and launch — with no Swift inside it, dlopen failing to
logcat and the view staying blank. The plugin's verifySwiftAndroidJniLibs
check fails the build with that explanation instead.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.plugin.compose")
id("sh.swifttui.android") version "0.9.11"
}
android {
namespace = "com.example.myapp"
// 37.1 or newer: both this AAR and Compose declare minCompileSdk 37.
compileSdk {
version = release(37) { minorApiLevel = 1 }
}
// Pin the NDK rather than taking AGP's default: it strips the packaged Swift
// `.so` files, and a default your machine has not installed fails the build.
ndkVersion = "27.3.13750724"
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 28
targetSdk = 36
// arm64-v8a is the 0.9 support tier. Without this the APK advertises ABIs
// it has no Swift runtime for, and installs-then-blanks on those devices.
ndk { abiFilters += "arm64-v8a" }
}
// Where the plugin stages libswift_tui_app_host.so and the Swift runtime.
sourceSets["main"].jniLibs.directories.add(
layout.buildDirectory.dir("generated/swiftJniLibs").get().asFile.path
)
packaging {
jniLibs {
// Extract the Swift runtime at install time so dlopen resolves it.
useLegacyPackaging = true
}
}
buildFeatures { compose = true }
}
// The one per-app Swift setting: which SwiftPM product to cross-build.
swiftTuiAndroidHost { productName = "MyAppHost" }
dependencies {
implementation("sh.swifttui:android-host:0.9.11")
implementation(platform("androidx.compose:compose-bom:2026.08.00"))
implementation("androidx.activity:activity-compose:1.13.0")
}3. SwiftPackage/Package.swift. The product must be a dynamic library:
the JVM owns the process and the Swift side is loaded with dlopen, so there is
no Swift entry point. SwiftPM names the output after the product, and the plugin
renames libMyAppHost.so to the canonical libswift_tui_app_host.so on the way
into the APK.
Author against SwiftTUIRuntime, not the SwiftTUI umbrella — the umbrella
pulls in terminal-only PTY primitives that do not compile for Android.
// swift-tools-version: 6.3
import PackageDescription
let package = Package(
name: "my-app-host",
platforms: [.macOS(.v15), .iOS(.v18)],
products: [
.library(name: "MyAppHost", type: .dynamic, targets: ["MyAppHost"])
],
dependencies: [
.package(url: "https://github.com/SwiftTUI/swift-tui.git", exact: "0.9.11")
],
targets: [
.target(
name: "MyAppHost",
dependencies: [
.product(name: "SwiftTUIRuntime", package: "swift-tui"),
.product(name: "SwiftTUIAndroidHost", package: "swift-tui"),
]
)
],
swiftLanguageModes: [.v6]
)4. The Swift entry point — your root View, wrapped in an App, behind the
one fixed symbol the AAR's JNI shim looks up:
import SwiftTUIRuntime
import SwiftTUIAndroidHost
struct MyRootView: View {
var body: some View { Text("Hello from SwiftTUI") }
}
struct MyApp: App {
var body: some Scene { WindowGroup { MyRootView() } }
}
@_cdecl("swift_tui_android_create_host")
public func swift_tui_android_create_host() -> Int64 {
MainActor.assumeIsolated {
do {
let host = try AndroidHostSceneHost(app: MyApp())
return AndroidHostHandleRegistry.register(host)
} catch {
return 0
}
}
}Mount the host in Compose:
setContent { SwiftTUIHostView(state = rememberSwiftTUIHostState()) }During the build, the plugin cross-compiles MyAppHost for arm64-v8a, renames
the result to libswift_tui_app_host.so, and stages it plus the Swift runtime
into build/generated/swiftJniLibs for the source set declared above.
swiftTuiAndroidHost.packageDirectory defaults to ../SwiftPackage, relative to
the app module — the MyApp/SwiftPackage/ in the layout above. Point it anywhere
else, and add any sources outside the package that should retrigger the Swift
cross-build:
swiftTuiAndroidHost {
productName = "MyAppHost"
packageDirectory = layout.projectDirectory.dir("../swift/Host")
additionalSwiftSources.from(layout.projectDirectory.dir("../shared/Sources"))
}- Install the Android SDK Platform 37.1 and NDK
27.3.13750724for the JNI shim. UseminSdk 28andcompileSdk 37.1— both this AAR and Compose1.12.0declareminCompileSdk 37, so a lowercompileSdkfailscheckAarMetadata. - Use AGP 9.1.0–9.2.1. Compose
1.12.0requires 9.1.0 or newer, and Android Studio 2026.1 refuses to sync a project on 9.3.x. Building one SDK ahead of the plugin is supported but warned about; silence it withandroid.suppressUnsupportedCompileSdk=37.1ingradle.properties. - Install Swift 6.3.x and the Swift Android SDK to cross-compile the host
.so. The plugin looks for the bundle under~/Library/org.swift.swiftpm/swift-sdks/; elsewhere (Linux, or a custom install) pointSWIFT_ANDROID_SDK_BUNDLEat the.artifactbundle. - Install the toolchains as described in the SwiftTUI documentation. The repository does not vendor these toolchains.
Maintainer build/test commands live in
docs/DEVELOPMENT.md. This repository builds and tests the
AAR and the plugin; it does not cross-compile Swift. The full host build — Swift
cross-compile, packaging, and an emulator install — lives in the counter demo's
AndroidExample,
which consumes these artifacts as a published dependency:
git clone https://github.com/SwiftTUI/swift-tui-counter-demo.git
cd swift-tui-counter-demo/AndroidExample
./gradlew :app:installDebug- Project site & framework API reference: https://swifttui.sh/docs/documentation/.
The
SwiftTUIAndroidHostSwiftPM product's reference is published there with the framework DocC. This README is the reference for the Kotlin/Gradle side (SwiftTUIHostView, theswiftTuiAndroidHost { }extension). - The framework:
SwiftTUI/swift-tui, the authoring API, products, and platform matrix. - Other hosts:
swift-tui-swiftui(native macOS/iOS) andswift-tui-web(browser). - Questions & issues: https://github.com/SwiftTUI/swift-tui-android/issues
MIT; see LICENSE.