BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Kotlin Multiplatform for iOS Developers

Kotlin Multiplatform for iOS Developers

Leia em Português

This item in japanese

Bookmarks

Key Takeaways

  • Using Kotlin Multiplatform, you can avoid repeating lots of logic to develop an app running on multiple platforms.
  • KMP is not the final step to accomplishing 100% shared code across all platforms since UI logic must still be programmed natively in many cases because it is too platform-specific to share.
  • The close similarities between Swift's and Kotlin's syntax greatly reduces a massive part of the learning curve involved with writing that KMP business logic.
  • You can use Android Studio to create a reusable KMP component that you later import into an Xcode project as a framework.

Kotlin Multiplatform for iOS Developers

DRY (or Don't Repeat Yourself) is one of the foundational principles of programming, but repeating lots of logic has been often necessary to develop an app running on multiple platforms. Trying to minimize repetition is just good programming. And more shared code across platforms means less repetition, which means better code.

Think about your iOS current project: is it available on any other platforms, like Android or the web? If so: how much logic does your iOS app share with its counterparts on those other platforms? If not, but making your app available on another platform is on the roadmap, how much will developing on the next platform force you to repeat yourself? Either way, the answer is probably: a lot.

Enter Kotlin Multiplatform (KMP). Kotlin is a statically-typed programming language that bears a striking resemblance to Swift, and is 100% interoperable with Java. In many ways, it's Swift for Android. KMP is a feature of Kotlin which shares code between an app's various platforms, so that each platform's natively programmed UI calls into the common code. KMP is not the final step to accomplishing 100% shared code across all platforms, but it is the natural next step towards that goal.

KMP works by using Kotlin to program business logic that is common to your app's various platforms. Then, each platform's natively programmed UI calls into that common logic. UI logic must still be programmed natively in many cases because it is too platform-specific to share. In iOS this means importing a .frameworkfile - originally written in KMP - into your Xcode project, just like any other external library. You still need Swift to use KMP on iOS, so KMP is not the end of Swift.  

KMP can also be introduced iteratively, so you can implement it with no disruption to your current project. It doesn't need to replace existing Swift code. Next time you implement a feature across your app's various platforms, use KMP to write the business logic, deploy it to each platform, and program the UIs natively. For iOS, that means business logic in Kotlin and UI logic in Swift.

The close similarities between Swift's and Kotlin's syntax greatly reduces a massive part of the learning curve involved with writing that KMP business logic. What's left in that learning curve is the IDE: Android Studio.

Kotlin Multiplatform Project is still an experimental feature of Kotlin; thus, APIs can change with every update. 

Getting Started

This tutorial is intended for iOS developers with little to no experience with Android Studio or Kotlin. If you don't have Android Studio yet, follow the installation guide.

Clone the starter project we’ve made for you. It contains a boilerplate KMP project, which includes an empty KMP library. The starter project also includes an iOS app and an Android app that both display gifs from a list of 25 hardcoded "meh" URLs. 

These apps are provided for you, but the library is not, because this article is exclusively focused on making a KMP library, and not the apps that use the library. But rest assured, you’ll acquire knowledge that’ll be valuable if you want to dive into Android development.

Using KMP, you're going to write networking logic that gets 25 URLs from Giphy by searching the phrase "whoa" in their public API. These URLs will replace the ones hardcoded into each platform.

Open Android Studio and select Open an existing Android Studio project. Through the Finder window that opens, select the top-level GifGetter/ directory of your cloned starter project.

If you see this additional dialog, press Update.

Android Studio

If you haven't delved into Android Studio before, it's probably a lot to take in. This is an introduction to the parts of Android Studio used in this tutorial. If you want a comprehensive introduction to the IDE, here it is.

Let's start with the basics: the project navigator on the left, which should show a file structure. If you don't see a file structure, select the Project tab towards the upper left hand corner, which will show the project navigator. At the top of the project navigator, you'll most likely see a dropdown menu labelled Android. Press it, and in the dropdown menu, select the Project option.  

  1. The row of buttons across the top of Android Studio is the toolbar. The tools in it have many functions, like building, running, debug running, applying new changes, and launching the Android Virtual Device (AVD) Manager. Beneath it is the navigation bar that shows where to find the currently open file in the project's folder structure, which in this case is /GifGetter/GifLibrary/commonMain/kotlin/platform/common.kt.
    • Here are some of the tools in the toolbar:
      • The hammer is the Build button
      • The dropdown next to it is the configurations
      • Next to it is Run button, just like in Xcode
  2. Android's official documentation refers to this as one of the tool windows, but it's usually referred to as the project structure, file structure, folder structure, or other similar terms. It's the direct corollary of Xcode's project navigator
    • Note the Project dropdown menu at the top. It should say Project for this tutorial. If it changes to Android or anything else, reselect Project or you may not see all your folders and files in the structure below.
  3. This is the editor window, where you write code. Note the files across the top: android.kt, common.kt, ios.kt, and GifLibrary, these are all the currently open background tabs. Right click on one of them to see options for opening more editor windows to the right, left, top, or bottom.
  4. This is part of the tool window bar, which wraps around the outside of Android Studio. The only relevant part is what's circled in yellow, particularly the Terminal and Build tabs.

Project Structure

Here's a broad overview of a KMP project's structure:

  1. .gradle/ and .idea/ folders: local files that should not be committed to source control for most Android Studio projects. They include local settings for the IDE, project, and dependencies, so they're not relevant to this tutorial.
  2. androidApp/ folder: the root directory for the Android app project in our larger KMP project that has all of the Android side's UI logic.
  3. build/ folder: contains output from your KMP builds. It should also not be committed to source control.
  4. iosApp/ folder: the root directory of the Xcode project in the larger KMP project.
  5. GifLibrary/folder: the home of the KMP logic. It will be the business logic for the Android project, and KMP will generate it into a GifLibrary.framework for the iOS project. The src/folder contains the following:
    • androidLibMain: Android platform-specific KMP logic
    • commonMain: KMP business logic that does not require any platform-specific code
    • iosMain: iOS platform-specific KMP logic that has the ability to interact with Apple APIs (ie: UIKit, GCD, etc...)
    • main: Contains a AndroidManifest.xml file, which defines GifLibrary/ as an Android Library. In many ways, it's like an Xcode.plist.
  6. build.gradle: This may be a new concept, because there's no direct counterpart for it in iOS development. In some ways, it's a build script or makefile that defines dependencies, scripts, and other settings for the project. In that way, it also somewhat like a .xcodeproj file.
  7. External Libraries: all of the project's dependencies. Android and KMP projects differ from iOS projects in that they require far more external dependencies.

The other files at the top level of the project (ie: local.properties and all of the .iml files) are generated by Android Studio and not relevant for what we'll be doing in KMP. On the other hand,  settings.gradle is a configuration file that should be included in source control. Unlike the build.gradle file, settings.gradle is not a build script, but a configuration file for gradle.

Now that you understand the basics of Android Studio it's time to dive into the KMP project!

Configuring Gradle for KMP

*NOTE: As mentioned before, KMP is still experimental, so these gradle configurations are highly subject to change.*

Open GifLibrary/build.gradle and look at the kotlin{} block: 

kotlin {
   targets {
       android()
       def onPhone = System.getenv('SDK_NAME')?.startsWith("iphoneos")
          if (onPhone) {
             iosArm64("ios")
          } else {
             iosX64("ios")
          }
   }
}

   sourceSets {
       commonMain {
           dependencies {
               implementation "io.ktor:ktor-client-json:$ktor_version"
           }
       }
   }
}

This is a fairly standard KMP gradle configuration for starting a project. First look at the targets {} block inside of the kotlin {} block. Without going into too much detail, this block is providing gradle with preconfigured settings for the Android and iOS platforms. iOS’s presets need extra lines because they’re different for device and simulator builds.

Next look at the sourceSets {} block. Right now, commonMain is the only sourceSet with any additional dependencies, but there are still two other sourceSets that need additional dependencies. After commonMain {} add the following to sourceSets {}:

androidLibMain {
   dependencies {
       implementation "io.ktor:ktor-client-json-jvm:$ktor_version"
   }
}

iosMain {
   dependencies {
       implementation "io.ktor:ktor-client-ios:$ktor_version"
       implementation "io.ktor:ktor-client-json-native:$ktor_version"
   }
}

 

After adding these, you should see a message bar saying: Gradle files have changed since the last project sync. A project sync may be necessary for the IDE to work properly. Ignore this message for now, because there are still more changes to be made to this gradle file.

Now all three required dependencies are included in sourceSets, but there is still one last change necessary to make before this gradle is ready.  Below the kotlin {} block, there is a task called copyFramework{} that copies the KMP project as an iOS framework into the appropriate folder, so the iosApp/ project can find it.

To ensure these tasks are performed when the project builds, insert the following line after copyFramework{}: 

tasks.build.dependsOn copyFramework

Now press Sync Now to update the gradle file. This might take some time if new dependencies need to be downloaded. You can check the progress of the gradle sync in the   tab at the bottom of Android Studio to see the sync's progress.

Exploring Giphy's API

Go to Giphy's API and create a Giphy developer account. Or, if you already have a Giphy developer account, just log in. Then, press Create an App and enter "GifGetter" when prompted, along with any description you choose. Copy your new Api Key.

Now, go back to Android Studio and open GifLibrary/src/commonMain/kotlin/GiphyAPI and find the following class:

class GiphyAPI {
   val apiKey: String = ""
}

Paste your Giphy API key into the value of this val statement, which is currently an empty string. This is how a class and string constant are declared in Kotlin. Notice that all of this code so far could be Swift, with the single exception of the val keyword, which is Kotlin's equivalent of a let.

Platform-specific Code

KMP accounts for differences between the iOS and Android platforms by telling the common code what to expect from the various platforms. Go to GifLibrary/src/commonMain/kotlin/platform/. Select File → New → Kotlin File/Class and create a new Kotlin File/Class and name it common. In the Kinddropdown menu below, select File.

These iOS and Android apps need to make networking calls, so they'll need to run asynchronous code. iOS uses Grand Central Dispatch (GCD) for such concurrency operations, but that can't go in the common code because Android doesn't use GCD. So instead, tell the common code what to expect from its platforms. In this case, it needs to expect the following dispatcher

import kotlinx.coroutines.CoroutineDispatcher

internal expect val dispatcher: CoroutineDispatcher

An error will appear because the iosMan/ and androidLibMain/ modules are now expecting a declaration of a constant named dispatcher of type CoroutineDispatcher. Start on the iOS side by going to GifLibrary/src/iosMain/platform/ and creating a new file called iOS and pasting in the following code:

internal actual val dispatcher: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue())

internal class NsQueueDispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() {
   override fun dispatch(context: CoroutineContext, block: Runnable) {
       dispatch_async(dispatchQueue.freeze()) {
           block.run()
       }
   }
}

This logic says that for iOS platform, KMP will use GCD and get the main queue to run the asynchronous code. Now go to the corresponding folder in the Android module: GifLibrary/src/androidLibMain/platform/. Create another Kotlin file and call it Android. Luckily, the Android platform makes it easy to get the main dispatcher:

internal actual val dispatcher: CoroutineDispatcher = Dispatchers.Main

The compile errors should disappear, as the dispatcherthat the commonMainexpects of both platforms is now supported with their respective actualimplementations.

Getting Ready for the Data

We need classes that can represent the JSON data we receive from Giphy's API. Create a new Kotlin file in GifLibrary/src/commonMain/kotlin/ and name it Data.  Paste the following code into your new file:

import kotlinx.serialization.Serializable

@Serializable
data class GifResult(
       val `data`: List<Data>
)

@Serializable
data class Data(
       val images: Images
)

@Serializable
data class Images(
       val original: Original
)

@Serializable
data class Original(
       val url: String
)

At the top of the file, Serializable must be imported to recognize the following @Serializable statements in front of the data classes. Think of data classes as structs in Swift. They aren't value type objects, but they behave similarly. The response JSON from the Giphy API will map onto these data classes. Now commonMain is ready for the bulk of the business logic.

Writing the KMP Business Logic

Go back to the GiphyAPI file and paste the following code under apiKey:

private val client: HttpClient = HttpClient { // 1
   install(JsonFeature) { // 2
       serializer = KotlinxSerializer(Json.nonstrict).apply { // 3
           setMapper(GifResult::class, GifResult.serializer()) // 4
       }
   }
}

private fun HttpRequestBuilder.apiUrl(path: String) { // 5
   url { // 6
       takeFrom("https://api.giphy.com/") // 7
       encodedPath = path // 8
   }
}

Here's a step-by-step breakdown of what's happening in this code:

  1. Declare a constant of type HttpClient. Once again, the only difference between this line and its equivalent in Swift is the keyword val, instead of let.
  2. Install JsonFeature into the client object to give it the ability to serialize and deserialize JSON.
  3. Instantiate and configure serializer, a property on HttpClient objects that is null by default. The Json.nonstrict format indicates that the response JSON will contain fields irrelevant to the data classes set on the next line.
  4. Provide the top-level data class for the serializer, which will then serialize the response JSON into a GifResult object. The "data" field in the JSON will populate the corresponding GifResult.data property. The fields will continue on down to populate the hierarchy of nested data classes.
  5. Add the function apiUrl(path: String) to the HttpRequestBuilder class.
  6. Construct a URL.
  7. Provide the base URL.
  8. Extend the URL with the specified path.

Now that the boilerplate networking logic is in place, it's time to make a networking call to Giphy's endpoint for "whoa" search results. Under the networking code you just pasted in, add the following:

suspend fun callGiphyAPI(): GifResult = client.get {
   apiUrl(path = "v1/gifs/trending?api_key=$apiKey&limit=25&rating=G")
}

But wait, what's the suspend keyword and why is it necessary? Well, it's one of the ways of executing asynchronous code in Kotlin through coroutines. For now, just know that this means that the function can only be called from a coroutine or another suspend function, such as the get{} function on the HttpClient.

The next step is to set up function that will be available for the iOS and Android apps. Once again, the syntax is very close to Swift:

fun getGifUrls(callback: (List<String>) -> Unit) { // 1
   GlobalScope.apply { // 2
       launch(dispatcher) { // 3
           val result: GifResult = callGiphyAPI() // 4
           val urls = result.data.map { // 5
               it.images.original.url // 6
           }
           callback(urls) // 7
       }
   }
}
  1. Declare the function getGifUrls(callback: (List<String>) → Unit). This function is what the Android and iOS code will call. The Unit return type is Kotlin's equivalent of a Swift void.
  2. Run the following code in the context of GlobalScope, which includes its own dispatcher and job cancelling logic.
  3. Launch our own dispatcher, not the default. Remember that the implementation of dispatcher is specific to each platform.
  4. Declare a value equivalent to what's returned from the callGiphyApi() function.
  5. Map the GifResult object's list of data...
  6. ... by reaching down until you get to the url property for each. The url is a string, so this will map into a List<String> object. The Kotlin it keyword represents the parameter of lambda, in the case that it only has one.
  7. Feed that list of URLs to the callback provided as a parameter of the function.

Implementing KMP on Android

The final step for the Android app is to replace its existing list of hardcoded URLs with URLs fetched from Giphy's API. This won't be as difficult as it might sound.

Open the file androidApp/src/main/kotlin/MainActivity. Android's Activity is closely correlated with ViewController, and the first function implemented in the MainActivity here is onCreate(savedInstanceState: Bundle?), which is somewhat approximate to viewDidLoad():

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   val gifList: RecyclerView = findViewById(R.id.gif_list)
   val layoutManager = LinearLayoutManager(this)
   gifList.layoutManager = layoutManager

   adapter = GifAdapter(this)
   gifList.adapter = adapter
   adapter.setResults(urls)
}

 

Delete the last line of the function where the results are being set with fakeData, and replace it with the following:

 

getGifUrls {
   adapter.setResults(it)
}

The setResults(results: List<String>) function will set the fetched gif URLs as the new data source and reload the RecyclerView, which is essentially the Android version of both UITableView and UICollectionView, although there are key differences that you should research further if you're interested. And that's it for all the logic on the Android side!

If it does not Auto Import and you have unresolved reference errors, you may need to add the following to the list of imports at the top of the file:

import org.gifLibrary.GiphyAPI

Run It on the Android Emulator

Press the Run button in the toolbar at the top of Android Studio. The icon is very similar to Xcode's Run button. A window will appear from the Android Virtual Devices (AVD) Manager, which is similar to Xcode's window for managing simulators. The difference is that Xcode comes with pre-downloaded simulators.

In the new window, if you see any Android devices available in the list, select that device to run the app. Otherwise, press Create New Virtual Device. Select the Phone category and Pixel 2 XL in the list of devices, then press Next in the bottom right.

Next, select Download next to the most recent version of Android, which will be the one with the greatest API Level. Another window will then appear to install the selected version of Android. If you don't see a Download button next to the most version of Android, you're in luck! Select that version and press Next again. Get a cup of coffee while the Android OS downloads, and when you come back with your new Android virtual device, select Finish.

All done on the Android side! Bask in the glory of your Android app and its KMP networking logic. Next it's back to familiar territory.

Xcode Setup

Open the Xcode project and go to the iosApp target's Build Phases and press the + button, circled in red here:

Select the New Run Script Phase option from the list that appears. You will see a new Run Script appear at the end of the list in the main window. Drag it upwards in the list so that it's above Compile Sources. Click the arrow next to it and paste in the following script:

cd "$SRCROOT/../"
./gradlew GifLibrary:copyFramework \
-Pconfiguration.build.dir="$CONFIGURATION_BUILD_DIR"          \
-Pkotlin.build.type="$KOTLIN_BUILD_TYPE"                      \
-Pdevice="$KOTLIN_DEVICE"

This run script reads the Xcode project's Build Settings for these definitions for use in the copyFramework gradle task, which you can see if you go back to GifGetter/GifLibrary/build.gradle. But the Xcode project still doesn't have these settings yet. To create them, go to the target's Build Settings tab and press the + button, as shown below:

Select Add User-Defined Setting in the little dropdown menu that appears. Name the new setting "KOTLIN_BUILD_TYPE". Click the arrow next to it to reveal the Debug and Release environments. Give Debug a value of "DEBUG" and Release a value of "RELEASE".

Add another User-Defined Setting, and name this one "KOTLIN_DEVICE". Then click the + button to the right of Debug. This will create a field underneath Debug that will say Any Architecture | Any SDK. Click on it to see a list of potential architectural options, and choose Any iOS Simulator SDK. Give that field a value of false. Do the same again for Debug, but this time add Any iOS SDK and give it a value of true.

Repeat these steps for Release under "KOTLIN_DEVICE". After all of this, your User-Defined Settings should look like this:

Next, in the search bar to the upper right, search for "Framework Search Paths":

Add the following to Framework Search Paths for both Debug and Release:

"$(SRCROOT)/../GifLibrary/build"

That path is where KMP outputs a .framework file that contains the business logic needed by the iOS app. If you go to GifGetter/GifLibrary/build/ in your Finder, you'll see a GifLibrary.framework file ready to be used by Xcode.

Next, go to the General tab of your iOSApp target, scroll to the bottom and press the + button under Embedded Binaries. In the window that appears, press Add Other at the bottom. In the Finder window that appears, select the GifLibrary.framework file in GifGetter/GifLibrary/build/ and press Open. This should also add the framework to Linked Binaries and Frameworks.

Open GifRetriever.swift and you'll see a hardcoded array of 25 URL strings. Beneath that, you'll see the requestGifs(_closure: @escaping StringsClosure) function passing back the hardcoded URLs. Underneath the import Foundation statement at the top of the file, add import GifLibrary.

Now, replace the body of the requestGifs(_closure: @escaping StringsClosure) function so that it looks like this:

func requestGifs(_ closure: @escaping StringsClosure) {
   GiphyAPI().getGifUrls { gifs -> KotlinUnit in
       closure(gifs)
       return KotlinUnit()
   }
}


The iOS app is ready to run in the Simulator! Now you've got an app on two different platforms which are sharing their networking logic with one another through KMP!

Next Steps

Check out the finished project on GitHub for reference, if you like. Remember, KMP is still experimental and highly subject to change, but it is rapidly evolving and improving. Not only that, KMP has already been used successfully to build apps currently available in the App Store. KMP is the natural next technology of choice for any iOS developer who wants to branch out to develop their apps on other platforms.

Not only is KMP the fastest route for an iOS engineer to become a dual threat mobile developer, it means creating an existing repository of business logic that you can then deploy to other platforms as well. If you wanted to then deploy the app to a Javascript environment, you already have business logic written for it.

If you want to dive deeper into KMP or into Kotlin in general, check out these resources, which include links to the Kotlin subreddit and Slack channel. Touchlab will be producing a webinar on the same subject as this tutorial on June 4th, 2019 1 - 2 PM EST. Finally, taking a step back, here are a couple of additional background articles on KMP from Touchlab that you might find useful:

About the Author

Ben Whitley is an iOS Developer based in NYC with 4 years of experience working in Xcode, Swift, and originally, Objective-C. For the past year and a half, he has been the primary iOS Developer at Touchlab, during which time he has worked closely with multiple high-profile clients. He is currently focused on furthering Kotlin Multiplatform as a native platform for iOS developers, and helping Android developers moving to KMP with their iOS- and Xcode-related questions.

Rate this Article

Adoption
Style

BT