Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

First Edition · Android 12, iOS 15, Desktop · Kotlin 1.6.10 · Android Studio Bumblebee

14. Creating Your KMP Library
Written by Carlos Mota

In the previous chapters, you’ve built learn for Android, iOS and desktop. All of these apps fetch the raywenderlich.com RSS feed and show you the latest articles written about Android, iOS, Flutter and Unity. You can search for a specific topic or save an article locally to read it later. During the app’s development process, you’ve worked with:

  • Serialization
  • Networking
  • Databases
  • Concurrency

And, along this journey, you’ve also built additional tools that can be reused in other projects:

  • Logger
  • Dispatchers

In this chapter, you’re going to learn how you can create and publish a library so you can reuse it in the other apps that you develop in this book — and for the next one you’re going to build. :]

Migrating an existing feature to Multiplatform

Throughout this book, you’ve learned how to develop a project that had a library already shared across different platforms. However, you may want to migrate an existing app to Kotlin Multiplatform.

In this section, you’re going to see how a simple feature like opening a website link in a browser can easily be moved to KMP.

Learning how to open a link in different platforms

In learn, when you click on an article, a web page opens — whether it’s on Android, iOS or desktop. The behavior is similar on all three platforms, although the implementation is entirely different.

In Android, a prompt is shown so you can select which app it should use to send the Intent. Or, if you have one already set as default, it will automatically open it and load the article you’ve clicked on. MainActivity.kt, in androidApp/ui, defines this function:

private fun openEntry(url: String) {
  val intent = Intent(Intent.ACTION_VIEW)
  intent.data = Uri.parse(url)
  startActivity(intent)
}

Since anyone can have multiple apps installed on a device, it’s important to define which apps are capable of receiving this intent. In this scenario, you’re looking for apps that can open a URL. To avoid opening the wrong app, Android allows you to define a couple of parameters the system uses to filter between all installed apps, and the one that best fit your Intent. First, it checks for those that have on their AndroidManifest.xml the ACTION_VIEW attribute defined, and then those that are capable to parse URIs.

iOS has a different approach. To open a URL, you just need to use the OpenURLAction from the environment. Open LatestView.swift from iosApp module and scroll to the Section struct:

@Environment(\.openURL) var openURL

The var openURL allows you to send a URL that will open the default browser on your device:

openURL(URL(string: "\(item.link)")!)

When the user clicks on one of the articles, the app creates a URL from that item link and calls openURL with it to open the link in the browser.

desktopApp uses another approach. Desktop has a browser function that you can use to launch the default browser on your computer. Open the Main.kt from the desktopApp module and scroll down to the end of this file:

fun openEntry(url: String) {
  try {
    val desktop = Desktop.getDesktop()
    desktop.browse(URI.create(url))
  } catch(e: Exception) {
    Logger.e(TAG, "Unable to open url. Reason: ${e.stackTrace}")
  }
}

The getDestkop call returns an instance of Desktop that contains its context as well as a couple of functions that let you access some of your computer’s features — like open and edit files, browser, mail, print, and more. Here, you’re using browse to open your default browser with the url from the item that you click on.

The trycatch block is necessary — according to the documentation, on some platforms, the desktop API might not be available. This might lead to unwanted behaviors. Following this approach guarantees that in the worst case, although the app won’t open a link, it also won’t crash.

Note: Alternatively, you could also use isDesktopSupported to check if the desktop API is available. In any case, be careful, because calling browse might trigger an IOException.

Now that you’re familiar with how the three platforms open a URL, it’s time to move this logic to KMP.

Adding a new module

The first thing to decide is if you want to move this logic to the existing shared module or create a new one. Since adding a new library also requires you to migrate the code, you’re going with this more complete solution.

The first step is to add a new KMM Module. Go to FileNewNew Module…, and select the Kotlin Multiplatform Shared Module template on the bottom of the list. Here, define the:

  • Module Name: shared-action
  • Package Name: com.raywenderlich.learn.action
  • iOS framework distribution: XCFramework

Click Finish and wait for the project to synchronize. Afterward, if you look at the Android Studio Project tab, you’ll see a new shared-action module added.

Open settings.gradle.kts file in the project root folder. Confirm that shared-action is now part of learn:

include(":shared-action")

The Android Studio template for Kotlin Multiplatform Mobile only generates the Android and iOS targets, so you’ll need to manually add the desktop platform.

In the shared-action module, open the build.gradle.kts file. In the kotlin section, after the listOf iOS targets, add:

jvm("desktop")

Now, go to sourceSets, and at the bottom add the desktopMain variable:

val desktopMain by getting

Synchronize the project and wait for this operation to finish.

You still need to add the desktopMain folders on the shared-action module. An easy solution to implement this is to right-click src and select NewDirectory. You’ll see a new window with a couple of folder suggestions. Search for “desktop” and select desktopMain/kotlin.

Now you’re just missing the package structure. You can easily create this directory by right-clicking desktopMain/kotlin. This time, select NewPackage. In this new window, enter: com.raywenderlich.learn.action.

You can also remove the Platform.kt and Greeting.kt files that Android Studio generated in the androidMain, commonMain and iosMain folders.

That’s it!

Your project structure will look like this:

Fig. 14.1 - Project structure
Fig. 14.1 - Project structure

Depending on the view type you have selected on the Android Studio project tab, you might have a different tree structure. To see the same one, select the Project option on top.

Fig. 14.2 - Android Studio project view
Fig. 14.2 - Android Studio project view

Configuring an Android library to publish

To publish the Android libraries, you need to make an update to the android() target definition in the kotlin section of the build.gradle.kts file from shared-action to:

android {
  publishLibraryVariants("release", "debug")
}

If you don’t define Android to publish its libraries, your project will use the one created for desktop by default. This is possible since JVM supports Android. However, this won’t work because the platform-specific code is entirely different on both platforms.

Configuring a Multiplatform Swift package

You have different possibilities to generate a library. Since Apple has its own package manager — Swift Package Manager — and many libraries are now available through it, you’re going to use it in this chapter.

However, there’s no official plugin to generate a Swift Package from a KMM project. So, you’ll need to use the Multiplatform Swift Package plugin. In the starter project, you’ve got a plugins’ folder that contains an updated version of this library. Open the settings.gradle.kts file to include it in the project:

includeBuild("plugins/multiplatform-swiftpackage-m1_support")

Note: This customized version of the plugin supports the Apple M1 architecture and uses the KMM XCFramework functions internally to generate the Frameworks.

Synchronize the project. Now, open the build.gradle.kts from shared-action, and add in the plugins section:

id("com.chromaticnoise.multiplatform-swiftpackage-m1-support")

You can define a couple of parameters to configure your Swift package. To accomplish this, add before the kotlin section:

//1
multiplatformSwiftPackage {
  //2
  xcframeworkName("SharedAction")
  //3
  swiftToolsVersion("5.3")
  //4
  targetPlatforms {
    iOS { v("13") }
  }
  //5
  outputDirectory(File(projectDir, "sharedaction"))
}

Here’s what’s happening:

  1. This is the function that allows you to configure your generated Swift package.
  2. You can define a specific name for the generated framework by setting the xcframeworkName. Otherwise, it will use the module’s name as default.
  3. As the name indicates, it corresponds to the Swift tools version required by the generated package.
  4. targetPlatforms defines which platforms and OS versions the framework should support. In this case, it’s going to work on all iOS devices and simulators that have version 13 or newer.
  5. By default, swiftpackage is the output folder of the Swift package. You can define a different location and name through the outputDirectory parameter.

This plugin allows you to generate the Swift Package Manager Manifest and the XCFramework that you can use on iosApp.

For the Framework to be generated as “SharedAction” you need to, additionally, set the XCFramework name, and it’s baseName. Go to the iOS target section and update:

val xcf = XCFramework("SharedAction")
listOf(
  iosX64(),
  iosArm64(),
  iosSimulatorArm64()
).forEach {
  it.binaries.framework {
    baseName = "SharedAction"
    xcf.add(this)
  }
}

Synchronize the project. Open the terminal, and in the project root folder, run:

./gradlew shared-action:createSwiftPackage

You should see:

BUILD SUCCESSFUL

Look at the shared-action folder. You’ll see a new sharedaction directory that only contains one file: Package.swift. Since the project doesn’t contain any code, no XCFrameworks were generated.

Create a PlatformAction.kt file inside shared-action/commonMain, and execute the createSwiftPackage command once again.

After its execution, return to the sharedaction folder. You now have the XCFrameworks for arm64 and arm64_x86_-_64 simulator.

That’s it! Your module is ready to generate Swift packages.

You can also make the same update on the shared module. Similar to what you’ve done on shared-action, open the shared/build.gradle.kts file and add the plugin:

id("com.chromaticnoise.multiplatform-swiftpackage-m1-support")

And then it’s configuration:

multiplatformSwiftPackage {
  xcframeworkName("SharedKit")
  swiftToolsVersion("5.3")
  targetPlatforms {
    iOS { v("13") }
  }
}

The above command allows you to only generate a Swift package for the shared-action module. If you want to generate for both shared modules, you can easily do it by not adding the library name as prefix:

./gradlew createSwiftPackage

Migrating the code to Multiplatform

Now that you’ve got everything configured, it’s time to move the code from the app’s UI to Multiplatform. Since the shared-action module is going to deal with the user action of opening a link, on the PlatformAction.kt that you’ve created inside commonMain, add:

public expect object Action {

    public fun openLink(url: String)
}

This is the object the UI will call.

Alternatively, you can just define the openLink function without adding it to an object:

public expect fun openLink(url: String)

You can call this function directly from androidApp and desktopApp, since you reference it directly. However, from iosApp, you would need to access it via:

PlatformActionKt.openLink(url: "\(item.link)")

Since there’s no class defined, the compiler creates one when generating the framework and uses the class name plus the extension of the file as its name.

Although you could define a different name, you’re always going to have the kt prefix — which isn’t the most sympathetic name, especially when you’re trying to convince the iOS team to adopt a shared module written in Kotlin. :]

Android Studio prompts a suggestion to automatically generate the missing files. Ignore it for now. In some IDE versions, this feature is not working as expected and ends up creating the files in wrong directories. To avoid any unexpected error, you’re going to add all of these files manually.

Create a PlatformAction.kt file for androidMain, iosMain and desktopMain. They should all be in the same directory for each of the platforms: com.raywenderlich.learn.action.

Define an empty actual function:

public actual object Action {

  public actual fun openLink(url: String) {}
}

Implement the openLink functions for the different targets:

  • androidApp: Open the MainActivity.kt file and scroll until you see an openEntry function. Copy its content and paste it on the openLink function you created in shared-action/androidMain:
public actual fun openLink(url: String) {
  val intent = Intent(Intent.ACTION_VIEW)
  intent.data = Uri.parse(url)
  startActivity(intent)
}

Since this code block exists outside the scope of an Activity, you need its Context to call startActivity. To overcome this, you’re going to declare an activityContext variable outside the Action object:

public lateinit var activityContext: Context

Now, update the startActivity call to:

activityContext.startActivity(intent)

You’ve got access to the Android SDK, so for the above function you’ll need to import:

import android.content.Context
import android.content.Intent
import android.net.Uri
  • desktopApp: Go to the Main.kt file and search for openEntry. Copy its content into the openLink function from shared-action/desktopMain.
public actual fun openLink(url: String) {
  try {
    val desktop = Desktop.getDesktop()
    desktop.browse(URI.create(url))
  } catch(e: Exception) {
    Logger.e(TAG, "Unable to open url. Reason: ${e.stackTrace}")
  }
}

The Logger class belongs to the shared module you’re not using on share-action. To solve this, you can do one of the following:

  • Replace the call to use println instead.
  • Add the shared library as a dependency. That’s excessive, though, since you’ll end up increasing the app size with unnecessary features.
  • Create a logger library and add it to shared-action.

For now, you’re going to follow the first approach. However, the third one is tempting, so don’t forget to do the first challenge of this chapter, and afterward come back to this step and replace the println function with Logger. :]

Replace the current Logger call with:

println("Unable to open url. Reason: ${e.stackTrace}")

And import:

import java.awt.Desktop
import java.net.URI
  • iosApp: Open the HomeView.swift file and search for openURL. You’ll find two results: the first one declares the variable from Environment, and the second one its invocation.

Moving this logic to shared-action/iosMain is more difficult than the previous ones because you’ll need to convert this code to Kotlin and find the corresponding iOS functions.

It’s important to remember that although you’re writing Swift code, KMM uses Objective-C signatures. This is why you use the NSLog on the PlatformLogger.kt file from shared/iosMain.

Occasionally, it can be difficult to find the module of a specific function. The native libraries follow the same structure as the ones from the iOS SDK, so the first step is to go to the official documentation website and change it to Objective-C:

Fig. 14.3 - Apple documentation for OpenURL function
Fig. 14.3 - Apple documentation for OpenURL function

In this image, you can find the openURL documentation:

  1. These dots show you the path where openURL is located. You can see that UIApplication belongs to UIKit if you click on them. So, to access this function from iosMain, you’ll need to import:
import platform.UIKit.UIApplication
  1. Open the UIApplication page. According to the documentation, the UIApplication is a singleton that you can access via sharedApplication. Therefore, to access openURL, you need to call:
UIApplication.sharedApplication.openURL(url)
  1. This drop-down allows you to switch between Swift and Objective-C.

Note: You can see all the classes that exist inside platform if you go to JetBrains’ kotlin-native repository.

Now that you learned how to implement openURL, return to PlatformAction.kt from shared-action/iosMain and update the existing openLink function with:

public actual fun openLink(url: String) {
  val application = UIApplication.sharedApplication
  val nsurl = NSURL(string = url)
  if (!application.canOpenURL(nsurl)) {
    println("Unable to open url: $url")
    return
  }

  application.openURL(nsurl)
}

When prompted, import the following libraries:

import platform.Foundation.NSURL
import platform.UIKit.UIApplication

Note: There’s currently an issue on the Android Studio for Mac M1 where the platform package seems to not be resolved. In other words, you might see the platform import along with the UIApplication and NSURL calls at red. If this is the case, don’t worry — you can compile the project without any problems.

That’s it! To compile the project and generate the JVM, Android libraries and XCFramework, run:

./gradlew assemble

And to generate the Swift Package (shared and shared-action), run:

./gradlew createSwiftPackage

Now you’ve got two libraries you can publish!

Adding a new library to the project

Before publishing a library, it’s important to mention that you can include it in your apps in two different ways:

Add as a new dependency

At the same level as shared, you include it on Android and desktop build.gradle.kts files and add it to the iOS app project.

On the androidApp and then on desktopApp build.gradle.kts files, in the dependencies section after the shared implementation, add:

implementation(project(":shared-action"))

Synchronize the project.

For iOS, you need to first open the project with Xcode. Remember that it’s the iosApp.xcworkspace file that you should load.

To simplify the process, follow these steps:

  1. Open the Project file and click the General tab on top.
  2. Scroll down to Frameworks, Libraries, and Embedded Content and click the plus sign below the SharedKit.xcframework.
  3. A new window will open asking you to choose the framework. Click Add Other… then Add Files…
  4. Navigate to ./shared-action/sharedaction/, select the SharedAction.xcframework and click Open.

Currently, shared is being added though the embedAndSignAppleFrameworkForXcode command from Run Script. Optionally, you can remove it and add it manually, following the same process as the one described above.

Your Xcode will have both frameworks added to the project:

Fig. 14.4 - XCode project view
Fig. 14.4 - XCode project view

Include inside the shared module

The existing shared module imports this library and makes its features available to all apps that use it.

This step is simpler to do — in this case you just need to add the shared-action as an implementation to the shared module build.gradle.kts file in the commonMain dependencies section:

implementation(project(":shared-action"))

To have a more strict separation of concerns, you’re going to follow the first option for learn.

Updating your apps to use your new library

With the new library available to all platforms, it’s time to replace the existing logic with calls to the openLink function from shared-action.

On androidApp, open the MainActivity.kt file and update the openEntry function to:

private fun openEntry(url: String) {
  activityContext = this
  openLink(url)
}

The activityContext that you’re setting here will be used to open a new activity from shared-action.

Additionally, don’t forget to remove the import:

import android.net.Uri

The next update that you need to do is on the Main.kt file on the desktopApp project. When invoking the MainScreen Composable, update the onOpenEntry call to:

onOpenEntry = { openLink(it) },

With this, you’re going to use the function from shared-action to open an article on your default browser. You can now remove openEntry at the end of this file and remove now-unnecessary imports:

import java.awt.Desktop
import java.net.URI
import java.net.URISyntaxException

To update the iOS app, switch to Xcode and make the same update on the following files:

  • RWEntryRow.swift
  • LatestView.swift

Remove the openURL declaration:

@Environment(\.openURL) var openURL

Replace the Button action when iterating over the items, from openURL to:

Action().openLink(url: "\(item.link)")

Don’t forget to the import the shared-action framework:

import SharedAction

And remove the url variable which is no longer necessary.

Now that you’ve updated the three platforms, compile and run the apps, browse through the articles list and select one to read.

Depending on your default browser, you’ll see screens similar to these:

Fig. 14.5 - Android app: Open an article
Fig. 14.5 - Android app: Open an article

Fig. 14.6 - Desktop app: Open an article
Fig. 14.6 - Desktop app: Open an article

Fig. 14.7 - iOS app: Open an article
Fig. 14.7 - iOS app: Open an article

Publishing your KMP library

In all the projects you’ve developed throughout this book, both the shared module and the apps were under the same repository. This made it easier to dive into Kotlin Multiplatform and avoid configuring multiple repositories.

With this, you can easily import any of them by just including it on the settings.gradle.kts file located in the project root directory:

include(":androidApp")
include(":desktopApp")

include(":shared")
include(":shared-action")

include(":pager")
include(":pager-indicators")
include(":precompose")

Each one of these includes represents a project that could be on a different repository. If you had them as a separate repository, you’d need to also set the project path:

include(":your-library")
project(":your-library").projectDir = file("../path/to/your-library")

Both scenarios present a couple of disadvantages:

  • The configuration is laborious. You need to add the projects both on settings.gradle.kts and on the build.gradle.kts of the project that will use them.
  • Higher build time — particularly the first time the project builds. This happens because there’s no library compiled at that moment.
  • There’s no versioning on these projects. If you want to use an older revision of the project, you need to manually checkout.

Alternatively to both scenarios, instead of including these modules, you can import its libraries either from a local maven repository or from a remote server.

In this section, you’ll publish the shared-action library that you created before.

Configuring a library

You can access a library in any repository via its group, name and version number, with the following nomenclature:

  • group:name:version

The project name is the folder name — in this case, shared-action. You can define the group and version on the build.gradle.kts file from shared-action.

To set the version, add the following parameter to build.gradle.kts :

version = "1.0"

The group, if not defined, uses the parent name. In this case, it would be learn. This can be a bit misleading, since there’s no information about the author. To overcome this, above version add:

group = "com.raywenderlich.shared"

How to publish a library locally

Open the build.gradle.kts file from shared-action, and at the end of the plugin section, add:

id("maven-publish")

And now to publish it locally, run on the terminal:

./gradlew shared-action:publishToMavenLocal

When the operation ends, you can read BUILD SUCCESSFUL in the console logs.

If you want to publish all your libraries locally, you should run instead:

./gradlew publishToMavenLocal

The default location for your local maven repository is on:

~/.m2/repository

Navigate to this folder, and you can see a com/raywenderlich/shared directory with the shared-action library for the different platforms inside.

Return to Android Studio. Before continuing, remove the include of the shared-action module from settings.gradle.kts:

include(":shared-action")

On the root of the learn project, open the build.gradle.kts. In the allprojects section, under repositories after google(), add:

mavenLocal()

The next time Gradle synchronizes, it will also look for the project dependencies in your .m2/repositories directory.

You’re just missing an update to the app’s dependencies. Open the build.gradle.kts files from androidApp and desktopApp and replace the entry:

implementation(project(":shared-action"))

that looks for the project, with:

implementation("com.raywenderlich.shared:shared-action:1.0")

That uses the library instead.

Compile both Android and desktop apps and open an article from the list.

Fig. 14.8 - Android app: Search for all Android articles
Fig. 14.8 - Android app: Search for all Android articles

Fig. 14.9 - Desktop app: Search for all Android articles
Fig. 14.9 - Desktop app: Search for all Android articles

How to publish a library to the GitHub Packages repository

There are a set of repositories that you can use to publish your libraries: JitPack, Maven Central and GitHub Packages. These are the most common. Or, you can always set up your own package repository.

Depending on the repository that you select, the configuration process should be similar to the one presented in this section. Typically, the differences are the URL that you use to connect to and the authentication required.

Here, you’re going to use GitHub Packages — mainly because it’s simple to configure and has a free tier that you can use. You just need to create an account.

Before you can publish a library, you need to first create the access token that Gradle will use to authenticate your account.

Create your access token

Log in to GitHub and go to your account Settings. You can see this option by clicking your avatar in the top right corner of the website. Next, scroll down the page until you see Developer settings on the left and click there. You’ll be redirected to a new screen. From there, go to Personal access tokens and then Generate new token.

Or, you can go directly to this link.

In this screen, you can configure a name for your token, how long it will be valid and which permissions it should have. For the name, add: Publish Maven Repository and check the write:packages and read:packages checkboxes.

It will automatically select the repo attribute. Your screen will be similar to this one:

Fig. 14.10 - GitHub token configuration
Fig. 14.10 - GitHub token configuration

Note: It’s important to choose a name that you can easily remember later on. It helps when you receive an email from GitHub saying that your token is about to expire and you need to decide whether you want to renew it or not.

Click Generate token. Copy the authentication token.

Create a new repository

To publish your libraries, you need a repository to push them. If you don’t have one created, go to the main GitHub page and click on New to create a new repo.

Alternatively, you can go directly to this link.

Write a repository name — for instance, shared-action — decide if you want to make it public or private, and select the Add a README file checkbox, so there’s already a branch created for you to use.

Publish your library

With the GitHub Package repository ready, return to Android Studio and open the gradle.properties file located in the root directory. Here, add your account username and the token that you copied earlier:

#Repository Credentials
mavenUsername=YOUR_USERNAME
mavenPassword=YOUR_TOKEN

These are the credentials that Gradle is going to use to authenticate.

Open the build.gradle.kts file from the shared-action module and scroll to the bottom.

After the multiplatformSwiftPackage, add:

publishing {
  repositories {
    maven {
      //1
      url = uri("https://maven.pkg.github.com/YOUR_USERNAME/YOUR_REPOSITORY")
      //2
      credentials(PasswordCredentials::class)
      authentication {
        create<BasicAuthentication>("basic")
      }
    }
  }
}

This Gradle task is responsible for publishing your libraries into the URL you defined. It’s using BasicAuthentication as the authentication mechanism:

  1. In this URL, you need to define:
  • YOUR_USERNAME: As the name implies, it’s the username of your GitHub account.
  • YOUR_REPOSITORY: The repository name that you chose before. If you followed the same naming convention, it should be shared-action.
  1. Gradle supports different types of authentication. You can find all of these methods on their documentation website. The PasswordCredentials::class looks at the mavenUsername and mavenPassword to authenticate the request.

Alternatively, you could define these variables directly here by replacing credentials with:

credentials {
  name = YOUR_USERNAME
  password = YOUR_TOKEN
}

It’s a good practice to have these in a separate file that should be added to the .gitignore file to avoid unconsciously pushing the credentials into the repository.

Before publishing your library, you need to add it once again to the settings.gradle.kts file. Open it and after shared add:

include(":shared-action")

Now that everything is ready, go to the terminal and enter:

./gradlew shared-action:publish

When this operation ends, you’ll see a BUILD SUCCESSFUL message in the console. Open your repository GitHub page and on the right side, you’ll see a section named Packages that should have a list of the libraries that you just uploaded. Go to the Packages section, and you’ll see a screen similar to this one:

Fig. 14.11 - GitHub published libraries
Fig. 14.11 - GitHub published libraries

Now that you’ve confirmed that your libraries were successfully uploaded, return to Android Studio and in build.gradle.kts that’s located in the root directory, add the following code after mavenCentral, which is inside the allProject/repositories section:

maven {
  url = uri("https://maven.pkg.github.com/cmota/shared-action")
  credentials(PasswordCredentials::class)
  authentication {
    create<BasicAuthentication>("basic")
  }
}

Previously, you defined the URL for publishing your libraries. Now, you’re adding to the list of repositories that Gradle should look into when downloading the project dependencies.

Note: To use the credentials that you defined on gradle.properties, you need to have this maven repository declared above the one from JetBrains. Otherwise, you’ll get an error related to missing credentials. This is due to the multiple declaration of maven repositories. Gradle automatically matches the maven repositories added with the credentials on gradle.properties, so the first one corresponds to mavenUsername/mavenPassword, the second one to maven2Username/maven2Password and so on.

And that’s it! The project is ready. Compile and run both apps: Android and desktop.

Fig. 14.12 - Android app: Browse through the latest articles
Fig. 14.12 - Android app: Browse through the latest articles

Fig. 14.13 - Desktop app: Browse through the latest articles
Fig. 14.13 - Desktop app: Browse through the latest articles

How to publish your Swift package

With your GitHub repository already configured from the section above, you can use it to also publish your Swift package.

Open the terminal and run:

./gradlew shared-action:createSwiftPackage

When the build ends, you’ll see a sharedaction directory inside the shared-action module that contains your frameworks. Copy the contents of this folder to your GitHub repository and push these files.

In the root of your repository, you should have the following files:

  • SharedAction.xcframework.
  • Package.swift.
  • README.md.
  • SharedAction-1.0.zip

Note: You can’t have them inside a folder. Otherwise, you won’t be able to add them easily to your project.

Open Xcode and go to Project and select the General tab. Scroll down to Frameworks, Libraries, and Embedded Content, and in case you’re still using the local SharedAction framework, remove it.

Next, click +, then Add Package Dependency on the bottom drop-down. A new window opens, and you can enter your repository URL in the top right corner. Depending on its visibility, Xcode might ask you for your GitHub credentials.

You’ll see a similar screen to this one:

Fig. 14.14 - XCode add a Swift package from a custom URL
Fig. 14.14 - XCode add a Swift package from a custom URL

On the Add to Project drop-down, select iosApp and then Add Package. Xcode will download your library.

If everything works as expected, you’ll see a second prompt asking you to confirm to add the SharedAction package or not:

Fig. 14.15 - XCode add a Swift package: Confirm
Fig. 14.15 - XCode add a Swift package: Confirm

Click AddPackage. When this operation ends, you can see the SharedAction framework added to the project.

Compile and run the app. There are new articles ready for you to read!

Fig. 14.16 - iOS app: Browse through the latest articles
Fig. 14.16 - iOS app: Browse through the latest articles

Challenges

Here are some challenges for you to practice what you’ve learned in this chapter. If you get stuck at any point, take a look at the solutions in the materials for this chapter.

Challenge 1: Create a logger

All the apps and the shared module use the Logger class defined on shared/PlatformLogger.kt. It’s a simple logger that calls on:

  • Android, the android.util.Log.
  • Desktop, the println.
  • iOS, the platform.Foundation.NSLog.

In this first challenge, create and publish a new library — shared-logger — that should contain the PlatformLogger.kt implementation for all the three platforms: Android, desktop and iOS.

Challenge 2: Integrate the logger library

Throughout this book, you created three different apps:

  • Find time, a time zone helper, in Section 1.
  • Organize, a multiplatform TODO app, in Section 2.
  • learn, an RSS feed reader for raywenderlich.com articles, in Section 3.

They each had a customized version of a logger. The second challenge is to use the library that you created from the first challenge, on all three apps. Don’t forget to make the changes both at the business logic and UI levels.

Challenge 3: Use the logger library in the shared-action module

At the beginning of this chapter, you successfully migrated the open links functions to Kotlin Multiplatform and created the shared-action module.

At the time, there was no logger class, so you used the println function as the module logger. With the recently created shared-logger, it’s now time to update shared-action and use your new library.

Key points

  • If the features you want to migrate to KMP have any platform-specific code, you need to write this specific logic for all the platforms your library will target.
  • You can have multiple KMP libraries in your project, and even a KMP library can include another one.
  • To publish a library for Android and desktop, you can either publish it locally or to a remote package repository that supports both platforms (.jar and .aar). In this book, you’ve seen how to use JitPack.
  • For iOS, you’re creating a Swift package to share your library. Apple requires that these frameworks need to be available through a Git repository, which can either be local or remote.

Where to go from here?

Congratulations! You’ve finished the last chapter of the book. Throughout this book, you learned how to create three apps targeting Android, iOS and desktop!

You started this journey by getting familiar with Jetpack Compose and Swift UI for UI development, and moved toward sharing your app’s business logic across these three platforms with Kotlin Multiplatform. You can now create an app from scratch and apply all of these new concepts, or migrate one that you’ve already written to KMP.

Now that you’re a Kotlin Multiplatform master, you might be wondering what to read next. Perhaps you want to dive deeper into Jetpack Compose and SwiftUI?

Or, do you prefer to sit back and watch a video course instead? You can see the Jetpack Compose and Your Second iOS & SwiftUI app that teach you the same concepts as the books.

Additionally, since you’re already familiar with Ktor, why not try it in another platform? One that doesn’t require you to design a UI: Server-Side Kotlin with Ktor. There are a lot more materials available for you to use as you learn — find them at raywenderlich.com.

Looking forward to seeing what you’re going to build next. :]

Stay safe, stay curious.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.