Guide: Creating a CocoaPod using Kotlin Multiplatform Mobile library
This post is about how we managed to create a usable Android .jar
and iOS CocoaPod using Kotlin Multiplatform Mobile, host it in a private repository and making it proper maintainable. At time of writing this, this is not properly supported yet by Kotlin Multiplatform Mobile.
Table of contents
- Introduction
- What is the problem?
- Determining options
- The journey
- Conclusion? Problem solved?
- Future improvements
- Resources
Introduction
At our company we are working on an app for a client which is native Android and native iOS. The client wanted us to implement an analytics library which will send tags to a dashboard. This way the client can see user flows and user interaction with the app, which can be used to improve user experience.
Now, since we are programmers, we don’t like to do things twice. In this case to have to manage these analytics tags in two places, so instead of going the easy way to make a list of strings in Android and a list of strings in iOS, we spend 3 days figuring out and developing a multiplatform statically typed library.
There is enough documentation about how to code a library, but there is not much documentation about configuring the project properly. That is what this guide is about. Check the Resources on the bottom for documentation on how to start with coding.
Furthermore, this guide assumes you have knowledge about how CocoaPods works and what it consists of. Otherwise you can refer to here.
It is also assumed you have knowledge about the Command Line Interface (CLI) and Git.
I have setup a project as an example for you to have alongside this guide. Feel free to clone and play with it here.
What is the problem?
We have two native apps with a requirement to send analytics tags. These tags (which are just strings) should be the same on both platforms. We want to maintain this list in one place efficiently and in a way we can potentially reuse it for different projects. This would also mean, ideally, that we would have statically typed tags instead of magic strings to catch errors on compile time.
Determining options
To come up with a solution to our problem we first had a little brainstorm session to check our options. We had some requirements to keep in mind:
- We would like statically typed resources, similar to
R
in Android andR.swift
in iOS, so we can catch errors at compile time - We would like a single source of truth for easy maintenance
- Resource should be easy to update
- The Android plugin and iOS pod should be easy to update
A colleague immediately mentioned Kotlin Multiplatform Mobile (KMM), but because we had not worked with that within our company we also looked at other solutions. Like, maybe we could import an excel sheet? Maybe we can use phrase.com (which we use to manage localisation) to also include these tags? What about a Flutter plugin?
Excel
First we thought of an excel sheet, this would be the simplest way to keep track of the tags in a single place, but this would have to be imported somehow and this would not grant the static typing we longed for, so this is not a solution.
Phrase
We briefly thought about putting the tags in phrase.com, but that is just a bit silly, since the purpose of the tags is totally different. This would grant the static typing, but the key-value pairs in phrase would look like screenA: "screenA"
which is not ideal, so we scratched this idea.
Flutter
Next, we had a little bit of experience making a mobile app with Flutter so we considered it, but this would mean working in a different language (Dart) which not everyone is familiar with and creating multiplatform libraries is also not really matured in Flutter. This was not an ideal option.
KMM
After weighing pros and cons, we decided to go with Kotlin Multiplatform Mobile (surprise surprise). This was heavily influenced by our Android developer and his curiosity in trying KMM, but also because of KMMs ability to generate an iOS CocoaPod using command line. Next to that it is an interesting choice to try a new multiplatform technology, so we went with it.
The journey
At the time of writing this, and doing this little project, KMM is still in alpha and documentation is limited. That’s why I hope this post will help other people wanting to do the same. This lack of documentation meant we ran into issues which were not yet addressed.
Basically the idea is to use KMM to have a single codebase where we define appropriate classes and strings for the tags. From this we can create a plugin for Android and a CocoaPod for iOS. Easypeasy, right?
Starting the project
IntelliJ IDEA would be the preferred editor to start a new KMM project with, together with the Kotlin plugin. Here you can find all the explanation to start with a new multiplatform plugin. Just make sure you select Mobile Library
when creating a new project.
Write your library
In src/commonMain/kotlin/<projct name>/
you can write your code you want to create a library for. In the ExampleProject
I created a class ExampleTag
with some example tags.
In build.gradle.kts
we set the library version and name:
val frameworkVersion = "1.0.0"
val frameworkName = "ExampleProject"
Android configuration
To compile a jar
file we have to add some configuration to build.gradle.kts
.
First we will add some sourceSets
for Android (should be already created):
Next we have to create an Android
function to compile the library to a jar
file. You can do this by adding the following function:
Now it is possible to create the jar
file for Android. Run the ./gradlew build
command, it will be created in /build/libs
.
In Android we create a jar
file and put this in our project manually, because of a lacking up-to-date artifactory. Add the generated *.jar
to the libs
directory in you Android project and you are good to go!
iOS configuration
For dependencies in iOS we use CocoaPods, so we need KMM to generate one for us, which conveniently is possible. We will generate a .framework
file and manually configure a .podspec
file. When this is done we can upload them to a repository for use with CocoaPods.
But first we need to configure the build.gradle.kts
file to use the appropriate iOS architectures. When running an iOS simulator on your Macbook the architecture used is x86_64 while real iPhones use the ARM64 architecture. In the build.gradle.kts
you will see a function kotlin
with within there android()
and iosX64("ios") { ... }
. This means Android and iOS are supported, but this iosX64
only supports simulators. This will mean you cannot run a project with this library on a real device, which we of course do want to do. So we will extend iosX64
with the following:
Next we have to configure sourceSets
. These are references to dependencies if you need them. Add this inside the kotlin
function. It will look something like this:
Last bit for us to do to be able to generate a proper framework, is to add tasks. These tasks will run when the gradle task to generate a framework will be fired. It will look something like this:
This will register the tasks universalFrameworkDebug
and universalFrameworkRelease
. We are gonna use this to build our framework.
This concludes the configuration for creation of the iOS framework. We set the appropriate architectures and made tasks to generate a framework.
Gradle tasks
Run the following commands in the root of the project to generate the framework:
./gradlew clean
— Deletes the build
directory./gradlew universalFrameworkRelease
— Generates the .framework
directory (in build/bin/universal/release/
)
Creating a podspec file
A .podspec
file is also necessary if you want to use your generated framework with cocoapods (If you don’t want to do this, there is also the possibility to manually add the framework to your XCode project).
Create a new file in IntelliJ
with the name <library name>.podspec
and add the following code:
Run through all the values and fill them out with your information. The thing you need to keep in mind is the spec.version
, this should be updated every time you make a change to this library. A little script can help with this. We have set the library version in build.gradle.kts
and it is possible to copy this version number to the .podspec
file, but we could also make a little update script to do it for us.
In the ExampleProject
root is a scripts
directory which contains a updateCocoapod.sh
script. It looks like this:
This script will get the version from the build.gradle.kts
file and replace the version in the ExampleProject.podspec
file with the newer version. You can run this script from the project root using sh scripts/updateCocoapod.sh
.
Almost there
So now we have the .framework
directory and the .podspec
file, everything we need to create a CocoaPod. The last thing to do is to upload it to a repository. Ray Wenderlich has a good guide on how to upload a CocoaPod here. It covers both uploading to private repositories and uploading your podspec to the CocoaPods Master Specs repository.
In the ExampleProject
I also added a script in the scripts directory called updateRepository.sh
, you can use this to easily push the .framework
and .podspec
to your repository. This will also tag the commit with the library version, if the library version is not bumped you will get an error.
How to use in XCode
Add your fancy new library to the Podfile
in an XCode project and run pod install
to start using it. Use it as follows:
Conclusion? Problem solved?
For us this was a frustrating and fun process, but in the end it was worth it. We now have a single source of truth for the tags and we can use them statically in Android and iOS. Next to that it is easy to update this multiplatform library with new tags and to create and upload a framework from it.
After we chose Kotlin Multiplatform Mobile as a solution, the main issue we ran into was the configuration of the iOS architectures. There is very limited documentation about this, although quite a crucial step in the usability. After figuring out we had to use iosX64
and iosARM64
, we were already much closer to our intended goal.
Next we had to upload the .framework
and .podspec
file to a repository, and while this is not thát much work, it would be easier to automate it. So a script was a solution and made it much easier to update the repository.
In the end it was a cool challenge and we are proud of the results. While it was not the easiest solution to our problem, I do recommend trying it out if you run into a similar situation we did.
And yes, problem solved 🕺🏻
Future improvements?
KMM is still in alpha so a lot of things can be changed by the time you are reading this, but that is why I wrote this guide. It might be a bit complex to set up and it is useful to have some reference.
Currently the process for Android is very manual, the compiled Android library is added to the project manually. That could certainly be improved by using something like artifactory to make it a dependency we can update by changing the library version, like we can do in iOS now using CocoaPods.
Resources
[1] https://kotlinlang.org/docs/tutorials/mpp/multiplatform-library.html
[2] https://kotlinlang.org/docs/tutorials/native/apple-framework.html
[3] https://kotlinlang.org/docs/reference/mpp-build-native-binaries.html#build-universal-frameworks
[4]https://kotlinlang.org/docs/reference/native/cocoapods.html
[5] https://github.com/Schroefdop/KMMExample