0% found this document useful (0 votes)
28 views30 pages

EXP10

This document describes how to build a multi-platform mobile app for Android and iOS using Kotlin Multiplatform. It explains how to integrate the common module, set up platform-specific dependencies, fetch network data from a shared API, and save data in a common way.

Uploaded by

ezhilnetworks
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
28 views30 pages

EXP10

This document describes how to build a multi-platform mobile app for Android and iOS using Kotlin Multiplatform. It explains how to integrate the common module, set up platform-specific dependencies, fetch network data from a shared API, and save data in a common way.

Uploaded by

ezhilnetworks
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 30

EX.

NO:10 Mini Project


A Mini Project involving Kotlin MultiPlatform for Android and iOS
Introduction
While these two platforms are different, the business logic behind your app is probably similar:
download files, read from and write to a database, send messages to a remote host, and retrieve and
display fancy kitten pictures.

These similarities are why Kotlin Multiplatform Mobile (KMM) exists. Historically, it’s known as Kotlin
Multiplatform Project, or MPP. Thanks to Kotlin/JVM, Kotlin/JS and Kotlin/Native, you can
compile/transpile a single project for many platforms.

Here ,you’ll learn how to build and update an app for Android and iOS while only having to write the
business logic once in Kotlin. More specifically, you’ll learn how to:

 Integrate KMM into an existing project.

 Set up the common module.

 Fetch data from a network.

 Save data.

Download the starter project by clicking the Download Materials button at the top or bottom of the
tutorial.

Open the starter project in Android Studio 4.1 or later.

Use a recent version of Kotlin — 1.4.20 or above. For iOS, you need Xcode 11.3 or later.

Note: This tutorial assumes you are familiar with the basics of Android and Android Studio. If you are
new to Android development, check out our Android Tutorial for Beginners series first.

Looking at the code, you’ll find the starter project provides the interface and some logic for the
MultiGrain app: A simple app that navigates a list of grains and their descriptions, it saves your
favorite grains and has platform specific code in the UI.

Build and run the starter code on Android Studio and you’ll see the screen below.
Once you click the Grains button, you’ll see the screen below. It has a hardcoded list of grains.
Of course, you’ll want to update the list from time to time, and it’s better if it can be done remotely.
It’s also great if you can save the users’ favorite grains locally. Since you have both Android and iOS
apps, you can save some headache by sharing some code for fetching the data and saving your
preferences.

But before you get into the implementation, you’ll learn some theory first.

Multiplatform
Kotlin Multiplatform supports more than just Android and iOS. It also supports JavaScript, Windows
and other native targets. You’ll focus on the mobile aspect, or Kotlin Multiplatform Mobile (KMM).

Common Code
KMM allows shared code in Android and iOS. On Android, it does this by using a shared module with
Gradle. On iOS, you can import the shared module as a framework that you can access from Swift
code.
If you think it’s easy to understand how Android uses common code, you’re right. It’s a
simple include in settings.gradle. However, as mentioned earlier, for iOS it’s different, as the code is
compiled to native code. You’ll go through the steps of creating this framework later.

Note: For more on this topic, you can read about understanding the KMM project structure on the
Kotlin website.
Not all code can be common; for example, calling native libraries for key-value storage requires
writing different code for iOS and Android. To do this, there can be a common interface acting as
common code. Then the platform specifies the interface that will be used in each platform
separately.

Platform-Specific Code
Sometimes you need to call methods that are specific to a platform. It isn’t possible to use platform-
specific code inside the common module, but Kotlin has a mechanism it uses to achieve this
result: expect/actual.

First, declare a class, method or function in the common module using the expect keyword, and
leave the body empty, as you often do when creating interfaces or abstract classes. Then, write the
platform-specific implementations for the class, method or function in all the other modules using
the actual keyword in the signature.
Each platform can share a common interface while also possessing its own implementation.

Now that you’ve covered the theory, what now? Keep reading to find out.

Integrating KMM Into an Existing Project


Open the starter project in Android Studio. The folder structure should look like the following.

Now you’ll begin integrating it into an existing project.

Setting Up the Common Module


Before continuing, ensure you have the KMM plugin in Android Studio. To check this, go
to Preferences > Plugins in Android Studio. Then look up Kotlin Multiplatform Mobile. Install it if it
isn’t already there.

Next, to add the common module, go to File > New > New Module.

Then select KMM Shared Module as shown below:

Check the Generate packForXcode Gradle task. This will be used later when you set up iOS.

Lastly, click on Finish.

To see if everything still works, build and run your project. It shouldn’t change anything visually, and
it should still compile.
Integrating Into Android
First, add the following line in androidApp/build.gradle inside the dependencies structure:

implementation project(":shared")

This will allow you to use the common module in Android. Later, you’ll also add dependencies and
platform-specific code.

Integrating Into iOS


Before you start integrating the common module in iOS, select iosApp as the configuration. Then
build and run the project.

It should open iOS Simulator and show the screen below.


To use the common module in iOS, open iosApp/iosApp.xcodeproj in Xcode. Next, add a Run
Script in Build Phases of the Xcode project by pressing the + button.

Enter the code below in the expanded Run Script area:

cd "$SRCROOT/.."

./gradlew :shared:packForXCode -PXCODE_CONFIGURATION=${CONFIGURATION}

Note: Make sure that after the cd line, the ./gradlew command is one line. There should be no line
breaks.
After the build script is entered, it should look like:

Then build the project in Xcode to create the common code as a framework. After this,
select general under the MultiGrain target. In Frameworks, Libraries, and Embedded Content, click
the + button.

On Choose frameworks and libraries to add, click the dropdown Add Other and select Add Files.
Afterward, navigate two folders up to the root starter folder, as shown below,
add shared/build/xcode-frameworks/shared.framework and click Open.

After adding the framework, it should look like the image below.
At this point, the project won’t be able to find the framework, so you have to add the file into
the Framework Search Paths. To do this, go to the target’s Build Settings. Then find Framework
Search Paths. Set the value of this field to:

$(SRCROOT)/../shared/build/xcode-frameworks

The change is shown in the picture below.

Build and run. There won’t be a visual change, but now you can start using the common module.

Close Xcode for now, as the next section is about fetching data from the network.

Fetching Data From the Network in Common Code


To fetch data, you need a way to use networking from common code. Ktor is a multiplatform library
that allows performing networking on common code. In addition, to parse/encapsulate the data, you
can use a serialization library. Kotlin serialization is a multiplatform library that will allow this from
common code.

To start, add the dependencies. Go back to Android Studio, open shared/build.gradle.kts and add
the following lines of code below the import but above plugins:

val ktorVersion = "1.5.0"

val coroutineVersion = "1.4.2"

You’ve defined the library versions to be used. Now, inside plugins at the bottom add:
kotlin("plugin.serialization")

Next, replace the code inside of sourceSets which is inside of kotlin with:

// 1

val commonMain by getting {

dependencies {

implementation("org.jetbrains.kotlin:kotlin-stdlib-common")

implementation(

"org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")

implementation(

"org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")

implementation("io.ktor:ktor-client-core:$ktorVersion")

// 2

val androidMain by getting {

dependencies {

implementation("androidx.core:core-ktx:1.2.0")

implementation(

"org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")

implementation("io.ktor:ktor-client-android:$ktorVersion")

// 3

val iosMain by getting {

dependencies {

implementation("io.ktor:ktor-client-ios:$ktorVersion")

Here’s what you’re doing in the code above:

1. Adding the dependencies to the common module.

2. Declaring the dependencies to the Android module.


3. Filling the dependencies into the iOS module.

Next, you need to add the data models. Create a file


named Grain.kt under shared/src/commonMain/kotlin/com.raywenderlich.android.multigrain.sha
red. Add the following lines inside this file:

package com.raywenderlich.android.multigrain.shared

import kotlinx.serialization.Serializable

@Serializable

data class Grain(

val id: Int,

val name: String,

val url: String?

This defines the data model for each grain entry.

Create another file inside the same folder, but this time, name it GrainList.kt. Update the file with
the following:

package com.raywenderlich.android.multigrain.shared

import kotlinx.serialization.Serializable

@Serializable

data class GrainList(

val entries: List<Grain>

This defines an array of grains for parsing later.

Since now you have the data models, you can start writing the class for fetching data.

In the same folder/package, create another file called GrainApi.kt. Replace the contents of the file
with:

package com.raywenderlich.android.multigrain.shared

// 1
import io.ktor.client.*

import io.ktor.client.request.*

import kotlinx.coroutines.GlobalScope

import kotlinx.coroutines.launch

import kotlinx.serialization.json.Json

// 2

class GrainApi() {

// 3

// 3

private val apiUrl =

"https://gist.githubusercontent.com/jblorenzo/" +

"f8b2777c217e6a77694d74e44ed6b66b/raw/" +

"0dc3e572a44b7fef0d611da32c74b187b189664a/gistfile1.txt"

// 4

fun getGrainList(

success: (List<Grain>) -> Unit, failure: (Throwable?) -> Unit) {

// 5

GlobalScope.launch(ApplicationDispatcher) {

try {

val url = apiUrl

// 6

val json = HttpClient().get<String>(url)

// 7

Json.decodeFromString(GrainList.serializer(), json)

.entries

.also(success)

} catch (ex: Exception) {

failure(ex)

}
}

// 8

fun getImage(

url: String, success: (Image?) -> Unit, failure: (Throwable?) -> Unit) {

GlobalScope.launch(ApplicationDispatcher) {

try {

// 9

HttpClient().get<ByteArray>(url)

.toNativeImage()

.also(success)

} catch (ex: Exception) {

failure(ex)

Here’s the breakdown of what the code above does:

1. Defines the imports for this class.

2. Creates the GrainApi class.

3. Declares the URL for the API.

4. Calls getGrainList() for fetching the JSON data.

5. Since Ktor needs to be called on a coroutine, this launches a lambda using a


dispatcher, ApplicationDispatcher. This needs to be defined before you can build the app
without errors.

6. Gets the JSON string from the URL.

7. Deserializes the string into a GrainList class.

8. Starts another method, getImage(), to fetch the image from common code.

Now that you’ve added the dependencies and data model and written a class for fetching the data,
it’s time to learn about using Expect in common modules.

Using expect in Common Modules


At this point, you have a few things that aren’t yet defined —
namely ApplicationDispatcher, Image and toNativeImage().

Create the file Dispatcher.kt in the same package. Then fill it in with the following:

package com.raywenderlich.android.multigrain.shared

import kotlinx.coroutines.CoroutineDispatcher

internal expect val ApplicationDispatcher: CoroutineDispatcher

Here you state the expectation that there will be platform-specific implementations of
this ApplicationDispatcher value.

Create another file in the same package and name it NativeImage.kt. Replace the contents of this file
with:

package com.raywenderlich.android.multigrain.shared

expect class Image

expect fun ByteArray.toNativeImage(): Image?

Here you declare an expected Image class and a method, toNativeImage(), which operates
on ByteArray and returns an optional Image.

At this point, you still can’t build the app since the expect declaration is missing
the actual counterparts.

Fetching Data in Android


To add the actual declarations, go
to shared/src/androidMain/kotlin/com.raywenderlich.android.multigrain.shared and create a file
named Dispatcher.kt. Insert the following lines into the file:

package com.raywenderlich.android.multigrain.shared

import kotlinx.coroutines.*

internal actual val ApplicationDispatcher: CoroutineDispatcher = Dispatchers.Default

You defined the expected ApplicationDispatcher variable. Next, create a file, NativeImage.kt, in the
same package or path:

package com.raywenderlich.android.multigrain.shared
import android.graphics.Bitmap

import android.graphics.BitmapFactory

// 1

actual typealias Image = Bitmap

// 2

actual fun ByteArray.toNativeImage(): Image? =

BitmapFactory.decodeByteArray(this, 0, this.size)

The code above:

1. Defines the Image class as an alias of Bitmap.

2. Declares the actual implementation of the extension function, toNativeImage(), which


creates a Bitmap from the array.

After adding a lot of files, you can now build and run your app even though parts of the code are still
underlined in red. There are still no visual changes, but check that your project is compiling
successfully.

Now you’ll wire GrainApi to the Android code.

Open MultiGrainActivity.kt inside androidApp. At the top of the class add:

private lateinit var api: GrainApi

This declares an api variable. Next, in onCreate replace the statement:

grainAdapter = GrainListAdapter()

with the two statements below:

api = GrainApi()

grainAdapter = GrainListAdapter(api)

Now you’ve initialized the api variable

and passed it to the constructor of GrainAdapter. This will cause an error until you
update GrainAdapter, which you’ll do after one more change here. Add the following inside the body
of loadList:

api.getGrainList(

success = { launch(Main) { grainAdapter.updateData(it) } },

failure = ::handleError

)
The code above adds a call to getGrainList inside loadList(). There are some error messages at the
moment, ignore them for now. These changes also require some changes on GrainListAdapter.kt.
Open this file and replace:

typealias Entry = String

with:

typealias Entry = com.raywenderlich.android.multigrain.shared.Grain

You’ve changed the typealias of Entry to refer to the class you wrote in the common code
called Grain. Now, add a parameter to the constructor of the class so that it looks like:

class GrainListAdapter(private val api: GrainApi) :

RecyclerView.Adapter() {

This changed the constructor of this adapter to include api. Locate the hardcoded grain list and
replace it with:

private val grainList: ArrayList<Entry> = arrayListOf()

The list is now an empty array so that the data can be provided via a call to the api instead. Lastly,
locate bind and replace the body with:

// 1

textView.text = item.name

item.url?.let { imageUrl ->

// 2

api.getImage(imageUrl, { image ->

imageView.setImageBitmap(image)

}, {

// 3

handleError(it)

})

The code above:

1. Sets the text to the item name.

2. Gets the image and sets it as a Bitmap.

3. Handles the error if it occurs.

After a lot of code without visual changes, build and run the app to see that the Android app is now
fetching data from the internet and loading the images as well. After clicking on the Grains button, it
should appear as shown below:
With that done, now it’s time to fetch data in iOS.

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:
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.multig
rain.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.multigr
ain.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.
Saving Data in iOS
Of course, this isn’t complete without the iOS part. Create a file
called KeyValueStore.kt under shared/src/iosMain/kotlin/com.raywenderlich.android.multigrain.s
hared. Then insert these lines:

// 1

actual typealias Controller = UIViewController

// 2

actual fun Controller.getBool(key: String): Boolean {

return NSUserDefaults.standardUserDefaults.boolForKey(key)

}
// 3

actual fun Controller.setBool(key: String, value: Boolean) {

NSUserDefaults.standardUserDefaults.setBool(value, key)

Similar to the code in Android, the code above:

1. Aliases Controller as UIViewController.

2. Writes getBool(), which reads a Boolean from NSUserDefaults.

3. Declares setBool() to set a Boolean on NSUserDefaults.

To show this, open MultiGrainsViewController+UITableView.swift in iosApp. Then modify it like


below:

extension MultiGrainsViewController: UITableViewDelegate {

// 1

func tableView(_ tableView: UITableView,

didSelectRowAt indexPath: IndexPath) {

let entry = grainList[indexPath.row]

let current = api.isFavorite(id: entry.id)

api.setFavorite(id: entry.id, value: !current)

tableView.reloadRows(at: [indexPath], with: .automatic)

extension MultiGrainsViewController: UITableViewDataSource {

func tableView(_ tableView: UITableView,

cellForRowAt indexPath: IndexPath) -> UITableViewCell {

...

// 2

cell.accessoryType = api.isFavorite(id: entry.id) ?

.checkmark : .none

return cell

}
}

Here’s a quick overview of what this code does:

1. Updates tableView(_, didSelectRowAt:) to toggle the Boolean for the item when the cell is
selected.

2. In tableView(_, cellForRowAt:), sets the accessoryType to show a check if the item is a


favorite.

Another small update is in MultiGrainsViewController.swift:

override func viewDidLoad() {

super.viewDidLoad()

api = GrainApi(context: self)

...

Now the API takes in a UIViewController.

Finally, build and run your iosApp in Android Studio. As with Android, you can now toggle your
preferences and check if the data is persisted after a restart of the app.
Thus the implementation of the Mini Project with Android and Kotlin Multiplatform is completed
Successfully

You might also like