After long time waiting and reading lots of articles on how multi platform project can help reduce identical business logic in your app, i’ve decided to try out Kotlin Multiplatform Project for Android and iOS.
There are 2 reasons why i’ve decided to wait before start trying:
Why not using Coroutines
? some of you might say 😁. Well for starter, we already have couple of tutorials using Coroutines
with Kotlin Multiplatform, and seconds, there are no reactive tutorial yet for multi platform project (as far as i know).
This tutorial provide an alternative approach building a multi platform project by using Reactive
instead of Coroutines
. This tutorial will take for about 1 to 2 hours of your time, prepare snacks and drinks a lot.
Enough with the talks, lets make our hand dirty.
Note:
Our goal is to make similar output and functionality on both Android and iOS by having the same business logic shared by Kotlin Multiplatform. This will be our final results:
Note: you need macOS compatible platform to run Xcode
This tutorial will use MVVM with input output approach inspired by kickstarters, (https://github.com/kickstarter/native-docs/blob/master/inputs-outputs.md).
Here’s a design diagram how our app structure will be.
At the time of writing, i am using the latest version of the following libraries:
For those who are using Kotlin 1.4.0 and above, i’ve updated the sample on Github. Just checkout on branch Kotlin1.4.0
Updated libraries:
Please note there’s still issue with multithreading on K/N, so make sure you’re using Coroutines version 1.3.9-native-mt
and not the 1.3.9
one.
Please also take a look on this issue regarding multi-thread coroutines:
https://github.com/Kotlin/kotlinx.coroutines/blob/native-mt/kotlin-native-sharing.md#known-problems
I need to remove ktor logging
as well since it is causing crash on iOS due to ktor last release has changed coroutines pattern usage and awaitAll
bug started to reproduce.
Most of the codes doesn’t change much, so you still can follow this article. All deprecated method also have been updated on the github.
Project preparation
Open up your Android Studio, and create an android project, next, create new module, and choose Android Library, and lets call it: Core
.
Now go to your new created project directory, and rename app
folder into android
folder. Close your android studio and re-open and go to settings.gradle
and change your :app
into :android
Note: Later if you add other platforms into the project, it is recommended that you put it at the same level with core
and android
directories.
Now lets start by adding dependencies into our multi platform projects. First open up projectbuild.gradle
and update it by adding kotlin-serialization
classpath and reaktive url into repositories. Your build.gradle
should be similar to this one:
Open up your core module’s build.gradle
, and delete all of its contents and replace with this one:
Then open your android build.gradle
and add exclude META-INF to remove warning.
packagingOptions {
exclude 'META-INF/*.kotlin_module'
}
Add implementation project(‘:core’)
into your dependencies. Your android build.gradle should be look like this:
freeCompilerArgs.add(“-Xobjc-generics”)
,commonMain
for our Kotlin Multiplatform and the other 2 are androidMain
and iOSMain
are for platform specific codes,experimental
, add useExperimentalAnnotation
to get rid of those warnings.Next, removed the androidTest
, main
, and test
directory inside core
directory, create commonMain
, androidMain
, and iOSMain
, and create kotlin
as a subfolder along with a package name directory inside kotlin
subfolder. Your directory structure should be like this:
Now that we’ve setup the project structure and gradle, try to build the project, and let’s continue to the next step.
Connecting to cloud service
We will connect to OMDb and get the movie list using ktor. API is free, you can get it from here:
https://www.omdbapi.com/apikey.aspx
We are going to search all movies contain avenger keyword with this url: http://www.omdbapi.com/?s=avenger&apikey=xxxxxx
Below are the JSON responses from the API:
Now that we know the responses, we will create DTO (Data Transfer Object), domain models and a transform class to map DTO to domain models:
How it works
Now after we’ve prepared our data models, we create Service Interface and its implementation class to connect to API server and get the result.
How it works
ktor
, we use build-in install method to install json feature and use default kotlin serializer. We also install logging feature to log the API request & response,apiUrl
in HttpRequestBuilder.apiUrl
by adding apiKey
to host url,Note: we’re using suspend function because its required when using ktor as most of its methods are based on Coroutines.
Update on 20 Sept 2020:
HttpResponse
has been deprecated on ktor 1.4.0, use the following code to get response from API.Repository
Repositories hide the details from outside on how the data is stored and retrieved. Data store can be SQL, Cloud or even file. Connection to and from data layer should only via repository.
Let’s create a repository interface.
Repository implementation
Now that we’ve finished preparing the data layer, your directory structure should be similar to this:
Use case
Use cases contains business logic, they can contain one or more repositories and provide result into our view.
Our use case for this tutorial are pretty simple contains only 1 repository to get the list we need:
Implementation
Coroutines Interop
ktor
make heavy use of coroutines to do asynchronous task, and we have to find a way to transform suspend fun into observable stream. Luckily Reaktive provide a Coroutines interop for us to do that. You can add this implemention inside your core’s multi platform build.gradle
.
implementation ‘com.badoo.reaktive:coroutines-interop:<latest-version>’
During example creation for iOS, i’ve found a problem that ktor does not return any response code using latest coroutines-interop v1.0.0. If you’ve experienced similar problem, you can use below code as a temporary fix.
Update on 20 Sept 2020:
singleFromCoroutine
from coroutines-interop:x.x.x-nmtc
instead of above code to prevent crash on iOS.Update on 20 Sept 2020:
singleFromCoroutine
from coroutines-interop:x.x.x-nmtc
instead of above code to prevent crash on iOSViewModel
ViewModel
represent the data that we want to display on our view. In this example, our view model will return list of movies.
Implementation
How it works
Inputs
represent any interaction or input from the view, while Outputs
represent changes from view model that the view has to display. Communication from/to view model can only happened via this exposed inputs and outputs,singleFromCoroutine
method to transform use case’s suspend fun into Single observable.result
. One thing that you should pay attention is that i’ve added mapper into view model constructor to map domain model into presentation model, .e.g.: Parcelable model in Android.Building Wrapper Class
Now that we’ve set up all layers, the last part before we moved to Android and iOS is creating a wrapper class. This class simply provide a method to access reaktive subscribe method so it is accesible on both platforms.
Now we are into UI parts of tutorial, lets building an android platform to display list of movies using recycler view.
Android
Lets start creating parcelable
model and its mapper.
Mapper
Next we’ll setup android activity.
Note: I won’t go into detail on how to setup adapter and view holder as its pretty straightforward process for android developer.
How its works:
ListViewModelImpl
, and since we’re going to use parcelable model, we also pass MovieModelsMapper
into ViewModel. Alternatively you can provide this view model through dependency injection using Dagger 2 or Koin,viewmodel.inputs.get
to start downloading movie list from the API,loading
and result
.iOS
If you are using macOS platform, you can continue building Kotlin Multiplatform for iOS, let’s start by building iOS framework from gradle. Type this command inside Android Studio Terminal:
After successful build, your can check your framework inside xcode-frameworks sub directory.
Now lets open Xcode, click create new project, and save it into iOS directory
There are a couple of setup before importing framework into your projects. First click your project and go to Frameworks, Libraries, and Embedded Content
, drag and drop Core.framework
from xcode-frameworks
directory into it.
Now go to Build Settings, search Framework Search Paths and put your xcode-frameworks
path. If you follow this example from beginning and use the same directory structure and name, you can type $(SRCROOT)/../core/build/xcode-frameworks
Last, go to Build Phases
, add New Run Script Phase
, move it below Dependencies
section, and add this bash script.
This run script will make sure that we will always get the latest framework code when building the app.
You should be able to import Core
after building the project.
Next we will setup ViewController
.
Again, i won’t go into detail how to setup UICollectionViewDataSource and UICollectionViewCell.
How its works:
UIRefreshControl
and UICollectionView
, and lazy init the View Model.NSString
instead of Swift’s String
. Using Swift’s String directly will give you an error ‘ListViewModelImpl’ requires that ‘String’ be a class type. This is one of the limitation that i’ve found so far using generics,ListViewModelImpl
mapper and use domain model from Kotlin Multiplatform project directly,ViewDidLoad
, we call binding()
method to subscribe into view model’s output: loading
and result
, and then at the end, we called viewmodel.inputs.get
to start downloading movie list from API,Movie
as ListViewModelImpl
’s generic, we always get Any as a return type,deinit
is called every time you pop or dismiss your View Controllerand…, we’re finished!
Thats pretty long journey 🤩, i hope you’re not getting lost and make it to last part successfully. You can stop here if you think you’ve already gotten what you wanted. However like everybody said, a good app should always give feedback in a bad / no internet connection. If you’re still have time, let’s take a look into the last part of this tutorial.
Caching is one of the important part when building a good app, and we’re very fortunate that Kotlin Multiplatform has a library called sqldelight to help us. (https://github.com/cashapp/sqldelight)
Lets start by adding sqldelight dependencies into our build.gradle
.
Start by opening project’s build gradle and add a new classpath into dependencies:
Now open, core’s build.gradle
, apply plugins and add sqldelight
in commonMain
and iOSMain
In the same core’s build.gradle
add the following script:
How it works:
sqldelight
support into our build.gradle
,sqldelight
database config inside core’s build.gradle
.Note: By default if not specified, sqldelight will use default Database as a name.
Next, create a directory inside commonMain
by following the package name and sourceFolders
we’ve setup before. Your directory structure should be like this:
Now, lets add a new file called Movie.sq
, copy the sql script, save inside com.adrena.core.sql
directory, and then build the project.
Note: sqldelight will automatically generate a kotlin script based on our provided sql syntax. In this example you can access insert query by calling insert()
You should see a generated kotlin code in your Android Studio similar to this:
Note: if your directory is not generated after building the project, try restarting your Android Studio.
Let’s continue by creating cache sub directory inside data directory, and create a database helper class.
How it works:
Movies.sq
in specific package directory matches our sqldelight config in build.gradle
,sqldelight
will auto-generate kotlin code,DatabaseHelper
class to provide access to sqldelight
from Android and iOS. Android should provide the driver directly from Android Project itself because it need Context as parameter, while iOS will provide the driver using Kotlin Native Sqlite
.Sql caching
Start by creating caching interface and its implementation
Implementation
How it works:
Repository class modification
Now let’s modify our repository class to return movie list from cache if available or request from API if cache is empty.
How it works:
Android
Add sqldelight
into Android’s build.gradle
Now open Main Activity
and update the following code:
Last, update your settings.gradle
How it works:
sqldelight
implementation into Android build.gradle
,DatabaseHelper
class using AndroidSqliteDriver
. Please note that this approach is not recommended. dbHelper should be singleton and should not be initialized in every activity,enableFeaturePreview(‘GRADLE_METADATA’)
. Do not forget to update this, otherwise gradle will throw error when you try to build iOS Framework.Try running your Android app. First time running it will fetch the movie list from OMDb, the next time you load, it will always load the list from your cache.
iOS
Run ./gradlew packForXCode
and open up your Xcode project. Open your AppDelegate
and add this code:
Next, open up your ViewController and update view model object:
Try running your iOS project, it should behave the same way just like Android.
Kotlin Multiplatform can help us build one business logic for Android and iOS, speeding up development process and help reducing unnecessary bugs because of different code base. However there’s always pros and cons when trying something new. Here what’s the list i’ve found during the creation of this tutorial.
Cons
Pros
As for me, this single pros beating all the cons that i mentioned above. In fact, i’m going to try multi platform on our next project. Wish me luck 😄.
Last but not least, you can get the code from github: