Kotlin Multiplatform Project for Android and iOS: Getting Started
In this tutorial, you’ll learn how to use Kotlin Multiplatform and build an app for Android and iOS with the same business logic code. By JB Lorenzo.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin Multiplatform Project for Android and iOS: Getting Started
30 mins
- Getting Started
- Multiplatform
- Common Code
- Platform-Specific Code
- Integrating KMM Into an Existing Project
- Setting Up the Common Module
- Integrating Into Android
- Integrating Into iOS
- Fetching Data From the Network in Common Code
- Using expect in Common Modules
- Fetching Data in Android
- Fetching Data in iOS
- Saving Data in SharedPreferences and UserDefaults
- Saving Data in Android
- Saving Data in iOS
- Where to Go From Here?
Fetching Data in iOS
Just as you did in Android, you need to define the actual implementation of the expected classes.
In shared/src/iosMain/kotlin/com.raywenderlich.android.multigrain.shared, create a file called Dispatcher.kt, and insert the following lines into the file:
import kotlin.coroutines.*
import kotlinx.coroutines.*
import platform.darwin.*
// 1
internal actual val ApplicationDispatcher: CoroutineDispatcher =
NsQueueDispatcher(dispatch_get_main_queue())
// 2
internal class NsQueueDispatcher(
private val dispatchQueue: dispatch_queue_t
) : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatchQueue) {
block.run()
}
}
}
Again, you defined the expected ApplicationDispatcher
variable, but this time in iOS. It’s a bit more complicated than Android. Since iOS doesn’t support coroutines, any calls to dispatch to a coroutine will be dispatched to the main queue in iOS. This is for simplicity.
The next task is to create a file, NativeImage.kt, in the same package or path:
import kotlinx.cinterop.*
import platform.Foundation.NSData
import platform.Foundation.dataWithBytes
import platform.UIKit.UIImage
actual typealias Image = UIImage
@ExperimentalUnsignedTypes
actual fun ByteArray.toNativeImage(): Image? =
memScoped {
toCValues()
.ptr
.let { NSData.dataWithBytes(it, size.toULong()) }
.let { UIImage.imageWithData(it) }
}
The code above does the following:
- Defines the
Image
class as an alias ofUIImage
. - Declares the actual implementation of the extension function,
toNativeImage()
, which creates aUIImage
from the bytes.
Build and run your app. There aren’t any visual changes, but make sure your project is working.
Déjà vu? Now you’ll wire the GrainApi
to the iOS code.
Open MultiGrainsViewController.swift inside iosApp. You can do this in Android Studio, so there’s no need to open Xcode. Add the import statement below the other import statements at the top of the file:
import shared
This imports the common module called shared
— recall that you added this framework earlier. Next, add the following lines inside MultiGrainsViewController
at the top of the class:
//swiftlint:disable implicitly_unwrapped_optional
var api: GrainApi!
//swiftlint:enable implicitly_unwrapped_optional
Now you’ve declared the api
variable and disabled the linter because you’ll be using forced unwrapping.
var grainList: [Grain] = []
This replaces the hardcoded grainList
with an empty array so it can be populated via the network call to the api.
Next, in viewDidLoad
, right below the call to super.viewDidLoad()
, add:
api = GrainApi()
The statement above initializes the api
variable. The last step is to replace the contents of loadList
with:
api.getGrainList(success: { grains in
self.grainList = grains
self.tableView.reloadData()
}, failure: { error in
print(error?.description() ?? "")
})
Now you’ve added a call to get the grain list inside loadList
, which updates the local grainList variable after a successful fetch. Then it reloads the table. On a failure, it shows an alert.
These also require some changes on MultiGrainsViewController+UITableView.swift. Open this file and in tableView
, replace the line:
cell.textLabel?.text = entry
with:
// 1
cell.textLabel?.text = entry.name
// 2
cell.imageView?.image = nil
// 3
api.getImage(url: entry.url ?? "", success: { image in
DispatchQueue.main.async {
cell.imageView?.image = image
cell.setNeedsLayout()
}
}, failure: { error in
// 4
print(error?.description() ?? "")
})
return cell
The code above does the following:
- In
tableView(_: cellForRowAt:)
, it sets the text to the item name. - Sets the image to
nil
. - Gets the image and sets it as an
image
of the cell’simageView
. - Handles the error if it occurs.
At this point, build and run the app to see that the iOS app is now fetching data from the internet and loading the images. After clicking on the Grains button, it should appear like this:
That was a lot of work already. You deserve a snack :] Go grab a bowl and put some muesli or oats or cereal in it, and add your liquid of choice.
Saving Data in SharedPreferences and UserDefaults
You’ve completed fetching data, but you’ll also want to use platform-specific methods, like key-value storage, in KMM. Since you still cannot save the user’s preferred grains, you can start by adding code to get and set favorites.
Open GrainApi.kt. Modify the constructor to look like:
class GrainApi(private val context: Controller) {
Now the constructor takes a Controller
instance, which you’ll define afterwards. Then, add the following to methods inside GrainApi
.
// 1
fun isFavorite(id: Int): Boolean {
return context.getBool("grain_$id")
}
// 2
fun setFavorite(id: Int, value: Boolean) {
context.setBool("grain_$id", value)
}
Here’s the gist of what the code above does:
- Defines
isFavorite()
, which will get a Boolean from the key-value store. - Declares
setFavorite()
to set a Boolean on the key-value store.
Since you modified the constructor with an undefined class, you should define it. Create a file called KeyValueStore.kt under shared/src/commonMain/kotlin/com.raywenderlich.android.multigrain.shared with the following inside:
package com.raywenderlich.android.multigrain.shared
// 1
expect class Controller
// 2
expect fun Controller.getBool(key: String): Boolean
expect fun Controller.setBool(key: String, value: Boolean)
This code declares an expected Controller
class together with two methods for setting and getting a Boolean.
Saving Data in Android
To save data in Android, create a file called KeyValueStore.kt under shared/src/androidMain/kotlin/com.raywenderlich.android.multigrain.shared called KeyValueStore.kt and insert the following:
package com.raywenderlich.android.multigrain.shared
import android.app.Activity
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
// 1
actual typealias Controller = Activity
// 2
actual fun Controller.getBool(key: String): Boolean {
val prefs: SharedPreferences = this.getSharedPreferences("", MODE_PRIVATE)
return prefs.getBoolean(key, false)
}
// 3
actual fun Controller.setBool(key: String, value: Boolean) {
val prefs: SharedPreferences = this.getSharedPreferences("", MODE_PRIVATE)
val editor = prefs.edit()
editor.putBoolean(key, value)
editor.apply()
}
Here’s a quick overview of what this code does:
- Aliases
Controller
asActivity
. - Writes
getBool()
, which reads a Boolean fromSharedPreferences
. - Declares
setBool()
to set a Boolean onSharedPreferences
.
Here you won’t see any difference when you run the app. To add a visual indicator for favorites, open MultiGrainActivity.kt in androidApp:
override fun onCreate(savedInstanceState: Bundle?) {
...
api = GrainApi(this)
...
}
...
private fun setupRecyclerView() {
...
toggleFavorite(item.id)
...
}
private fun toggleFavorite(id: Int) {
val isFavorite = api.isFavorite(id)
api.setFavorite(id, !isFavorite)
}
You updated GrainApi
, since now it takes an Activity
. And you also updated toggleFavorite()
to include the item ID. Moreover, you defined the contents of toggleFavorite()
; it toggles favorite
for this specific ID.
Now open GrainListAdapter.kt, and update the bind method by adding these lines at the top:
// 1
val isFavorite = api.isFavorite(item.id)
// 2
textView.setCompoundDrawablesWithIntrinsicBounds(
null,
null,
if (isFavorite) ContextCompat.getDrawable(
view.context, android.R.drawable.star_big_on)
else null,
null
)
This gives you the favorite status of the item. Then you set the corresponding drawable.
Build and run the app now. You should be able to toggle favorites by clicking on the grains. Click on a few and you’ll see your preferences persist. You can even restart the app to check.