Kotlin Integration & Gradle Build Automation

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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:

bboqsu geopj kioxfWwozmIhw (hehq) Jniumaz .fu zutxohoam Devepinuy Ruhe (ufbak wa foisyew) MObwnukbRronnZbusec (zmijt axq fediteqiw) gpumy woisl (wockaqig) Siriod sa mniDaxf Srofyu bijbiwav Pusjax + Lugu Zepsoled esre ISP Yeisg wa owywasm!
Loczsava bauwn rivubera wmer Jyobno ma UQN

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.

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()
    }
  }
}
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.

./gradlew clean

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")

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 ...
}

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.

cd taskmanager-lib
swift package resolve
# 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
# Switch to JDK 21
sdk use java 21.0.5-tem

# Verify
java -version
# Should show: openjdk version "21.0.5"

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!

Using Generated Classes in Kotlin

Now the generated TaskValidator class is available in Kotlin! Let’s use it in the repository.

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 ...
}

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
   }

What You’ve Accomplished

In this segment, you’ve:

See forum comments
Download course materials from Github
Previous: Swift-Java Integration Next: Testing & Validation