Menu

Dependencies

Skip project dependencies come in four forms:

  1. A dependency on a dual-platform Skip package.
  2. A dependency on a Swift Package Manager package used only by your iOS code.
  3. A dependency on a Kotlin or Java package library used only by your Android code.
  4. A dependency on common functionality implemented by separate iOS and Android libraries.

Read on for a discussion of each use case.

This chapter applies to dual-platform apps and frameworks. If you have created separate iOS and Android apps, add dependencies to each app in the standard way for its platform. The Getting Started guide additionally discusses how to add dependencies from your Android app to your shared Skip frameworks.

Adding Skip Package Dependencies

Use Package.swift to add a dependency on an external Skip SwiftPM package, just as you would for a standard Swift package. Skip will detect the presence of the dependency’s Skip/skip.yml file and add a corresponding Gradle dependency on the local transpiled output of that project. In other words, adding an SwiftPM dependency on the Swift side will automatically create a Gradle dependency on the Kotlin side. So when your project depends only on other Skip projects, you generally don’t need to perform any additional customization in your own skip.yml file.

Kotlin Package Names

Skip transforms your CamelCase Swift module names into dot-separated lowercase Kotlin package names:

  • MyPackage becomes my.package
  • MyHTTPLibrary becomes my.http.library
  • Kotlin package names must have at least two segments, so Product becomes product.module

Skip also removes the Tests suffix from module names during translation, so that your tests and source end up in the same Kotlin package. Kotlin does not have an equivalent of Swift’s @testable attribute, so this is the only way to allow your tests to access internal module API.


Adding Swift Dependencies

Add any iOS-only SwiftPM packages to Package.swift in the standard manner. When Skip fails to find a Skip/skip.yml file in a module, it excludes that module from the resulting Gradle project. Thus the dependency will only exist on the iOS side. You will therefore have to confine the package’s use to your iOS build with Skip compiler directives:

#if !SKIP
import SomeIOSPackage
#endif

#if !SKIP
... use iOS-only API ...
#endif

Adding Java/Kotlin Dependencies

Sometimes the Android side of a project might need to utilize external Java or Kotlin libraries that aren’t provided by the Skip frameworks. An example of this would be a Skip framework that provides a unified dual-platform API, but whose underlying implementation depends on an external project artifact.

As a concrete example, the SkipScript framework relies on the built-in JavaScriptCore framework on iOS (and thus has no external dependencies on the iOS side), but on Android it depends on external jsc-android libraries that contain the script engine and other supporting functionality.

In this case, you can use skip.yml to add dependencies to the Gradle side of the Skip project. We discuss skip.yml and Gradle here.

For example, here is the skip-script/Sources/SkipScript/Skip/skip.yml file containing parameters that specify its dependencies on external jsc libraries. These will be aggregated and included in the resulting build.gradle.kts file:

# the blocks to add to build.gradle.kts
build:
  contents:
    - block: 'dependencies'
      contents:
        - 'implementation("org.webkit:android-jsc-cppruntime:r245459@aar")'
        - 'implementation("org.webkit:android-jsc:r245459@aar")'

# the blocks to add to settings.gradle.kts
settings:
  contents:
    - block: 'dependencyResolutionManagement'
      contents:
        - block: 'repositories'
          contents:
            - 'maven("https://github.com/jectivex/jsc-android/raw/main/releases")'


In many cases, dependencies will be available from the built-in mavenCentral and google repositories. When a dependency is provided by one of these built-in repositories, only the build block needs to be added with a list of dependencies to add to the project, and the settings block can be excluded.

The resulting build.gradle.kts file contains the aggregated blocks from this module and all the dependent modules, and will look something like this:

// build.gradle.kts generated by Skip for the SkipScript module.
// This file is generated by the Skip transpiler plugin and is
// derived from the aggregate Skip/skip.yml files from the SwiftPM project.
// Edits made directly to this file will be overwritten.
dependencies {
    testImplementation("org.json:json:20180813")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
    androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
    testImplementation("org.robolectric:robolectric:4.10.3")
    androidTestImplementation("androidx.test:runner:1.5.2")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
    androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit")
    testImplementation("androidx.test:core:1.5.0")
    androidTestImplementation("androidx.test:core:1.5.0")
    testImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    implementation("net.java.dev.jna:jna:5.13.0@aar")
    testImplementation("net.java.dev.jna:jna:5.13.0")
    implementation("org.webkit:android-jsc-cppruntime:r245459@aar")
    implementation("org.webkit:android-jsc:r245459@aar")
    implementation(project(":SkipFoundation"))
    implementation(project(":SkipLib"))
    testImplementation(project(":SkipUnit"))
    androidTestImplementation(project(":SkipUnit"))
    implementation(project(":SkipFFI"))
}

plugins {
    kotlin("android") version "1.9.0"
    id("com.android.library") version "8.1.0"
}

kotlin {
    jvmToolchain(17)
}

android {
    namespace = group as String
    compileSdk = 34
    defaultConfig {
        minSdk = 29
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
    testOptions {
        unitTests {
            isIncludeAndroidResources = true
        }
    }
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>() {
    kotlinOptions {
        suppressWarnings = true
    }
}

tasks.withType<Test>().configureEach {
    systemProperties.put("robolectric.logging", "stdout")
    systemProperties.put("robolectric.graphicsMode", "NATIVE")
    testLogging {
        this.showStandardStreams = true
    }
}

And in this case, since the settings block was used, the resulting settings.gradle.kts file will include the customized repositories:

// This is the top-level Gradle settings for the project.
// The module dependencies it contains may be symbolic links to peer folders.
//
// This file is generated by the Skip transpiler plugin and is
// derived from the aggregate Skip/skip.yml files from the SwiftPM project.
// Edits made directly to this file will be overwritten.
//
// Open with External Editor to build and run this project in an IDE.
//
pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
        google()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
        google()
        maven("https://github.com/jectivex/jsc-android/raw/main/releases")
    }
}

rootProject.name = "skip.script"
include(":SkipScript")
include(":SkipFoundation")
include(":SkipLib")
include(":SkipUnit")
include(":SkipFFI")

The generated build files will be overwritten the next time the Skip transpiler is run, and so the generated output shouldn’t be edited directly. Instead, any changes to the module’s Skip/skip.yml properties will result in the build.gradle.kts and settings.gradle.kts being regenerated and included as part of the build.

When you’re using an Android-only package, confine it to your Android build with Skip compiler directives:

#if SKIP
import com.xyz.SomeAndroidType
#endif

#if SKIP
... use Android-only API ...
#endif

To import everything in a Kotlin package, use import com.xyz.__ instead of Kotlin’s import com.xyz.*. The latter is not valid Swift.

Dependencies on Separate iOS and Android Libraries

You often want to use functionality that has existing libraries for both iOS and Android, but does not yet have a Skip cross-platform library available. Most popular third-party frameworks publish both iOS and Android versions, and a plethora of other common functionality is available from independent developers on both platforms. To access this functionality, you have several options:

  1. Conditionally import and call the iOS or the Android library API at each usage site. This is only viable if you don’t use the functionality in many places.
  2. Write a simple common wrapper API within your application that delegates to the appropriate platform API internally.
  3. Create your own Skip dual-platform library. The advantage of this approach is that you can reuse your library in other projects, and contribute it to the open-source community for other Skip users to use and improve.

Regardless of which option you choose, the implementation strategy is the same:

  1. If you’re creating a library, follow the Getting Started instructions for new dual-platform frameworks.
  2. Add a Swift Package dependency on the iOS library, and add a Java/Kotlin dependency on the Android library you want to use.
  3. If you’re writing a common wrapper class or library, write the wrapper API.
  4. Within the implementation code, use Skip compiler directives to conditionally import and call the API for the appropriate library.

The resulting implementation pattern looks something like this:

#if !SKIP
import SomeIOSLibrary
#else
import com.xyz.someandroidlibrary.__
#endif 

public struct MyCommonAPI {
    #if !SKIP
    private let libraryInstance = SomeIOSLibraryType()
    #else
    private let libraryInstance = SomeAndroidLibraryType() 
    #endif

    public func myCommonAPIFunc() -> String {
        #if !SKIP
        return libraryInstance.someIOSLibraryFunction()
        #else
        return libraryInstance.someAndroidLibraryFunction()
        #endif
    }

    ...
} 

You can see this pattern clearly in the source of Skip frameworks like SkipKeychain, which is a great example to learn from.

When writing a dual-platform library that vends the functionality of a standard or well-known iOS framework, consider exactly mirroring that framework’s iOS API, just as Skip mirrors Foundation, SwiftUI, etc. This follows Skip’s philosophy of transparent adoption. Then rather than writing to your custom API, users can write to the exact iOS API. At the usage site, only the import will be different:

#if !SKIP
import StandardIOSLibrary
#else
import SkipVersionStandardIOSLibrary
#endif

// Use iOS library API... 

This pattern also means you don’t have to write any docs, nor do you have to implement any wrapper code for iOS! The iOS side of Skip apps will build directly against the existing iOS library. Skip’s FireStore support source is a good example of this.

And again, if you create a dual-platform library, please consider contributing it to the open-source Skip user community!