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.

4.7 (6) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. Defines the Image class as an alias of UIImage.
  2. Declares the actual implementation of the extension function, toNativeImage(), which creates a UIImage 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:

  1. In tableView(_: cellForRowAt:), it sets the text to the item name.
  2. Sets the image to nil.
  3. Gets the image and sets it as an image of the cell’s imageView.
  4. 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:

iOS app with fetched data

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:

  1. Defines isFavorite(), which will get a Boolean from the key-value store.
  2. 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:

  1. Aliases Controller as Activity.
  2. Writes getBool(), which reads a Boolean from SharedPreferences.
  3. Declares setBool() to set a Boolean on SharedPreferences.

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.

Android Grain List with Favorites