Kotlin Integration & Gradle Build Automation
Your Swift code generates Java bindings, but Android doesn’t know about them yet. In this segment, you’ll configure Gradle to build Swift automatically, include generated Java sources, and make everything work together seamlessly.
Understanding the Build Pipeline
Here’s the complete build flow you’re about to configure:
Every step is automatic - you just run ./gradlew build and everything happens!
Configuring taskmanager-lib Gradle Module
You already have a basic build.gradle.kts in taskmanager-lib/ from the previous segment. Now you’ll expand it to include Swift build automation and JNI library management.
Open taskmanager-lib/build.gradle.kts and replace its contents with:
import java.nio.file.Files
plugins {
id("com.android.library")
}
android {
namespace = "com.kodeco.android.taskmanagerkit"
compileSdk = 35
defaultConfig {
minSdk = 28
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("org.swift.swiftkit:swiftkit-core:1.0-SNAPSHOT")
}
// Helper function to get swiftly executable path
fun getSwiftlyPath(): String {
val fromConfig = project.findProperty("swiftly.path")?.toString()
?: System.getenv("SWIFTLY_PATH")
if (fromConfig != null) {
return fromConfig
}
// Try to find swiftly in common locations
val homeDir = System.getProperty("user.home")
val possiblePaths = listOf(
"$homeDir/.swiftly/bin/swiftly",
"$homeDir/.local/share/swiftly/bin/swiftly",
"$homeDir/.local/bin/swiftly",
"/usr/local/bin/swiftly",
"/opt/homebrew/bin/swiftly"
)
for (path in possiblePaths) {
if (file(path).exists()) {
return path
}
}
throw GradleException("swiftly not found. Please install swiftly or set SWIFTLY_PATH.")
}
fun getSwiftSDKPath(): File {
val fromConfig = project.findProperty("swift.sdk.path")?.toString()
?: System.getenv("SWIFT_SDK_PATH")
if (fromConfig != null) {
return file(fromConfig)
}
val homeDir = System.getProperty("user.home")
val possiblePaths = listOf(
"$homeDir/Library/org.swift.swiftpm/swift-sdks/",
"$homeDir/.config/swiftpm/swift-sdks/",
"$homeDir/.swiftpm/swift-sdks/"
)
for (path in possiblePaths) {
if (file(path).exists()) {
return file(path)
}
}
throw GradleException("Swift SDK path not found. Set swift.sdk.path in gradle.properties.")
}
// List of Swift runtime libraries
val swiftRuntimeLibs = listOf(
"swiftCore",
"swift_Concurrency",
"swift_StringProcessing",
"swift_RegexParser",
"swift_Builtin_float",
"swift_math",
"swiftAndroid",
"dispatch",
"BlocksRuntime",
"swiftSwiftOnoneSupport",
"swiftDispatch",
"Foundation",
"FoundationEssentials",
"FoundationInternationalization",
"_FoundationICU",
"swiftSynchronization"
)
val sdkName = "swift-6.3-DEVELOPMENT-SNAPSHOT-2026-01-16-a_android.artifactbundle"
val swiftVersion = "6.3-snapshot-2026-01-16"
val minSdk = android.defaultConfig.minSdk ?: 28
// Android ABIs and their Swift triple mappings
val abis = mapOf(
"arm64-v8a" to mapOf(
"triple" to "aarch64-unknown-linux-android$minSdk",
"androidSdkLibDirectory" to "swift-aarch64",
"ndkDirectory" to "aarch64-linux-android"
),
"armeabi-v7a" to mapOf(
"triple" to "armv7-unknown-linux-android$minSdk",
"androidSdkLibDirectory" to "swift-armv7",
"ndkDirectory" to "arm-linux-android"
),
"x86_64" to mapOf(
"triple" to "x86_64-unknown-linux-android$minSdk",
"androidSdkLibDirectory" to "swift-x86_64",
"ndkDirectory" to "x86_64-linux-android"
)
)
val generatedJniLibsDir = layout.buildDirectory.dir("generated/jniLibs")
val swiftSdkPath = "${getSwiftSDKPath().absolutePath}/$sdkName"
// Main task that builds Swift for all ABIs
val buildSwiftAll = tasks.register("buildSwiftAll") {
group = "build"
description = "Builds the Swift code for all Android ABIs."
inputs.file(file("Package.swift"))
inputs.dir(file("Sources/TaskManagerKit"))
outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${projectDir.name.toLowerCase()}"))
val baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile
if (!baseSwiftPluginOutputsDir.exists()) {
baseSwiftPluginOutputsDir.mkdirs()
}
Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).forEach {
if (it.toString().endsWith("JExtractSwiftPlugin/src/generated/java")) {
outputs.dir(it)
}
}
}
// Create a build task for each ABI
abis.forEach { (abi, info) ->
val capitalizedAbi = abi.split("-").joinToString("") {
it.replaceFirstChar { char -> char.uppercase() }
}
tasks.register<Exec>("buildSwift$capitalizedAbi") {
group = "build"
description = "Builds the Swift code for the $abi ABI."
doFirst {
println("Building Swift for $abi (${info["triple"]})...")
}
outputs.dir(layout.projectDirectory.dir(".build/${info["triple"]}/debug"))
workingDir = layout.projectDirectory.asFile
executable = getSwiftlyPath()
args("run", "swift", "build", "+$swiftVersion", "--swift-sdk", info["triple"]!!)
}
buildSwiftAll.configure {
dependsOn("buildSwift$capitalizedAbi")
}
}
// Copy .so files to jniLibs directory
val copyJniLibs = tasks.register<Copy>("copyJniLibs") {
dependsOn(buildSwiftAll)
abis.forEach { (abi, info) ->
// Copy the built .so files
from(layout.projectDirectory.dir(".build/${info["triple"]}/debug")) {
include("*.so")
into(abi)
}
// Copy libc++_shared.so from NDK
from(file("$swiftSdkPath/swift-android/ndk-sysroot/usr/lib/${info["ndkDirectory"]}/libc++_shared.so")) {
into(abi)
}
doFirst {
println("Copying Swift runtime libraries for $abi...")
}
// Copy the Swift runtime libraries
swiftRuntimeLibs.forEach { libName ->
from("$swiftSdkPath/swift-android/swift-resources/usr/lib/${info["androidSdkLibDirectory"]}/android/lib$libName.so") {
into(abi)
}
}
}
into(generatedJniLibsDir)
}
// Add the generated Java sources and JNI libraries
android {
sourceSets {
getByName("main") {
java {
// Add JExtractSwiftPlugin's generated Java files
srcDir(buildSwiftAll)
}
jniLibs {
// Add native libraries
srcDir(generatedJniLibsDir)
}
}
}
}
// Make sure Swift builds before Android
tasks.named("preBuild") {
dependsOn(copyJniLibs)
}
// Extend clean task to also remove Swift build directory
tasks.named("clean") {
doLast {
val buildDir = layout.projectDirectory.dir(".build").asFile
if (buildDir.exists()) {
println("Cleaning Swift build directory: ${buildDir.absolutePath}")
buildDir.deleteRecursively()
}
}
}
Also create taskmanager-lib/gradle.properties with:
android.useAndroidX=true
Clean the Project
After making these significant build configuration changes, it’s important to clean the build cache to avoid Gradle build failures.
In Android Studio, go to Build > Clean Project. Or from the terminal:
./gradlew clean
This clears out any cached build artifacts that might conflict with the new Swift integration.
What this configuration does:
- getSwiftlyPath(): Finds your swiftly installation automatically.
- getSwiftSDKPath(): Locates the Swift SDK for Android.
- swiftRuntimeLibs: Lists all Swift runtime libraries needed at runtime.
- abis map: Defines Android architectures and their Swift target triples.
- buildSwiftAll: Main task that builds Swift for all architectures.
- buildSwift{Abi}: Individual tasks per architecture (ARM64, ARMv7, x86_64).
- copyJniLibs: Copies compiled .so files and Swift runtime libs to jniLibs.
- sourceSets: Tells Gradle where to find generated Java sources.
- preBuild dependency: Ensures Swift builds before Android compilation.
Registering the Module with Gradle
Now tell Gradle about your new module. Open settings.gradle.kts at the project root and add:
rootProject.name = "Swift SDK for Android - Lesson 1"
include(":taskmanager-lib") // ADD THIS LINE
include(":app")
This registers taskmanager-lib as a Gradle module that the app can depend on.
Adding Dependencies to the App
Open app/build.gradle.kts and add these dependencies at the beginning of the dependencies block:
dependencies {
// TaskManager Swift library
implementation(project(":taskmanager-lib"))
implementation("org.swift.swiftkit:swiftkit-core:1.0-SNAPSHOT")
// Kotlin (existing)
implementation("androidx.core:core-ktx:1.15.0")
// ... rest of dependencies ...
}
What these dependencies do:
- implementation(project(”:taskmanager-lib”)): Links your app to the Swift module.
- implementation(“org.swift.swiftkit:swiftkit-core:1.0-SNAPSHOT”): Adds SwiftKitCore runtime support.
After adding these dependencies, Android Studio will prompt you to sync Gradle. Click Sync Now.
Publishing SwiftKitCore to Local Maven
Before building the Android app, you need to publish SwiftKitCore - the Java runtime library that swift-java uses. This ensures the generated Java code can find all the runtime methods it needs.
Navigate to your taskmanager-lib directory:
cd taskmanager-lib
First, resolve Swift package dependencies (this downloads swift-java to .build/checkouts/):
swift package resolve
Now publish SwiftKitCore using JDK 25:
# Set JAVA_HOME to JDK 25 temporarily for publishing
export JAVA_HOME="$(sdk home java 25.0.1-amzn)"
# Publish SwiftKitCore (takes ~15 seconds)
./.build/checkouts/swift-java/gradlew \
--project-dir .build/checkouts/swift-java \
:SwiftKitCore:publishToMavenLocal
You should see: BUILD SUCCESSFUL
Switch back to JDK 21 for all future work:
# Switch to JDK 21
sdk use java 21.0.5-tem
# Verify
java -version
# Should show: openjdk version "21.0.5"
Why this order? SwiftKitCore must match the version of swift-java that generated your Java wrappers. By publishing it right after
swift build, you ensure they’re in sync.
Why JDK 25 then JDK 21? The Gradle task to publish SwiftKitCore requires JDK 25, but Kotlin 2.1.0 (used by Android) requires JDK 21 or earlier. You only use JDK 25 for this one publishing step.
Understanding the Complete Build Process
When you run ./gradlew build, here’s the complete sequence:
Step 1: Gradle starts build
↓
Step 2: preBuild task runs
↓
Step 3: copyJniLibs task runs (depends on buildSwiftAll)
↓
Step 4: buildSwiftAll runs (depends on buildSwift{Abi} tasks)
↓
Step 5: buildSwiftArm64V8a, buildSwiftArmv7, buildSwiftX86_64 run in parallel
│
├─→ swiftly run swift build +6.3-snapshot --swift-sdk aarch64-unknown-linux-android28
│ └─→ Compiles TaskValidator.swift
│ └─→ JExtractSwiftPlugin RUNS
│ ├─→ Scans public Swift functions
│ ├─→ Generates TaskValidator.java
│ ├─→ Generates TaskValidator+SwiftJava.swift
│ └─→ Outputs to .build/plugins/outputs/
│ └─→ Creates libTaskManagerKit.so
│
└─→ (same for other ABIs)
↓
Step 6: copyJniLibs copies all .so files to jniLibs/
↓
Step 7: Gradle picks up generated Java sources (from sourceSets configuration)
↓
Step 8: Gradle compiles Kotlin + Generated Java together
↓
Step 9: APK is created with:
├─→ Kotlin classes
├─→ Generated Java classes (TaskValidator.java compiled)
└─→ Native libraries (all .so files in jniLibs/)
↓
Step 10: APK ready to install!
Everything is automatic - Swift builds, Java generates, everything compiles together!
Using Generated Classes in Kotlin
Now the generated TaskValidator class is available in Kotlin! Let’s use it in the repository.
Open app/src/main/java/.../repository/TaskRepository.kt and update the addTask method:
import com.kodeco.android.taskmanagerkit.TaskValidator // Add this import
class TaskRepository {
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
fun addTask(title: String, description: String, priority: Priority): Result<Unit> {
// Validate title using Swift
if (!TaskValidator.validateTitle(title)) {
return Result.failure(Exception("Title must be between 3 and 50 characters"))
}
// Validate description using Swift
if (!TaskValidator.validateDescription(description)) {
return Result.failure(Exception("Description must be between 10 and 200 characters"))
}
// Validation passed - create task
val task = Task(
id = java.util.UUID.randomUUID().toString(),
title = title,
description = description,
priority = priority,
isCompleted = false
)
_tasks.value += task
return Result.success(Unit)
}
// ... rest of class ...
}
What this code does:
- Imports TaskValidator: The auto-generated Java class from JExtractSwiftPlugin
- Calls validateTitle(): Passes the title to Swift for validation
- Calls validateDescription(): Passes the description to Swift for validation
- Returns failure: If validation fails, returns an error with a message
- Creates task: If validation succeeds, creates the task and adds it to the list
Notice how natural it looks - you’re calling TaskValidator.validateTitle() just like any other Kotlin/Java class! No JNI code, no type conversion, no bridge classes. It’s all handled by the generated code.
How Data Flows Between Kotlin and Swift
When you call TaskValidator.validateTitle("Hi"), here’s the complete journey:
1. Kotlin (TaskRepository.kt)
TaskValidator.validateTitle("Hi")
↓
2. Generated Java (TaskValidator.java)
public static boolean validateTitle(String title) {
return TaskValidator.$validateTitle(title); // Calls native method
}
↓
3. JNI (TaskValidator+SwiftJava.swift)
@_cdecl("Java_com_kodeco_android_taskmanagerkit_TaskValidator__00024validateTitle...")
- Receives jstring
- Converts to Swift String
- Calls Swift function
↓
4. Swift (TaskValidator.swift)
public static func validateTitle(_ title: String) -> Bool {
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.count >= 3 && trimmed.count <= 50
}
↓
5. Return path (back through JNI)
- Swift returns Bool
- Converted to jboolean
- Returns to Java
- Returns to Kotlin
↓
6. Kotlin receives result
if (!TaskValidator.validateTitle(title)) {
return Result.failure(...) // Shows error
}
All the type conversion and marshaling happens automatically in the generated +SwiftJava.swift file!
What You’ve Accomplished
In this segment, you’ve:
- Configured taskmanager-lib/build.gradle.kts with Swift build automation.
- Set up per-ABI Swift compilation tasks.
- Published SwiftKitCore to your local Maven repository.
- Configured Gradle to copy native libraries to jniLibs.
- Added generated Java sources to Android source sets.
- Registered taskmanager-lib module in settings.gradle.kts.
- Added dependencies to app/build.gradle.kts.
- Used TaskValidator from Kotlin code.
- Understood the complete build pipeline from Swift to APK.
Your Swift code is now fully integrated with your Android app! In the next segment, you’ll build the app, test the validation on a device, and see how everything works together.