Kotlin Help

Upgrade your app

You've already implemented common logic using external dependencies. Now you can add more complex logic. Network requests and data serialization are the most popular cases to share with Kotlin Multiplatform. Learn how to implement them in your first application so that after completing this onboarding journey, you can use them in future projects.

The updated app will retrieve data over the internet from a SpaceX public API and display the date of the last successful launch of a SpaceX rocket.

Add more dependencies

You'll need the following multiplatform libraries in your project:

  • kotlinx.coroutines for using coroutines to write asynchronous code, thus allowing simultaneous operations

  • kotlinx.serialization for deserializing JSON responses into objects of entity classes used to process network operations

  • Ktor framework as an HTTP client for retrieving data over the internet

kotlinx.coroutines

To add kotlinx.coroutines to your project, specify a dependency in the common source set. For that, add the following line to the build.gradle.kts file of the shared module:

sourceSets { val commonMain by getting { dependencies { // ... implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2") } } }

Multiplatform Gradle plugin automatically adds a dependency to the platform-specific (iOS and Android) parts of kotlinx.coroutines.

You'll also use the new memory manager for Kotlin/Native, which will become default soon. For this, add the following at the end of the build.gradle.kts file:

kotlin.targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java) { binaries.all { binaryOptions["memoryModel"] = "experimental" } }

kotlinx.serialization

For kotlinx.serilization, you need the plugin required by the build system. The Kotlin serialization plugin is shipped with the Kotlin compiler distribution, and the IntelliJ IDEA plugin is bundled into the Kotlin plugin.

You can set up the serialization plugin with the Kotlin plugin using the Gradle plugins DSL by adding this line to the plugins block at the very beginning of the build.gradle file in the shared module:

plugins { // kotlin("plugin.serialization") version "1.6.21" }

Ktor

You can add Ktor the same way you've added the kotlinx.coroutines library. In addition to specifying the core dependency (ktor-client-core) in the common source set, you also need to:

  • Add the ContentNegotiation functionality (ktor-client-content-negotiation) responsible for serializing/deserializing the content in a specific format.

  • Add the ktor-serialization-kotlinx-json dependency to instruct Ktor to use the JSON format and kotlinx.serialization as a serialisation library. Ktor will expect JSON data and deserialize it into a data class when receiving responses.

  • Provide the platform engines by adding dependencies on the corresponding artifacts in the platform source sets (ktor-client-android, ktor-client-darwin).

val ktorVersion = "2.0.2" sourceSets { val commonMain by getting { dependencies { // ... implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") } } val androidMain by getting { dependencies { implementation("io.ktor:ktor-client-android:$ktorVersion") } } val iosMain by creating { // ... dependencies { implementation("io.ktor:ktor-client-darwin:$ktorVersion") } } }

Create API requests

You'll need the SpaceX public API to retrieve data and a single method to get the list of all launches from the v4/launches endpoint.

Add data model

In shared/src/commonMain/kotlin, create a new RocketLaunch.kt file and add a data class which stores data from the SpaceX API:

import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class RocketLaunch ( @SerialName("flight_number") val flightNumber: Int, @SerialName("name") val missionName: String, @SerialName("date_utc") val launchDateUTC: String, @SerialName("success") val launchSuccess: Boolean?, )
  • The RocketLaunch class is marked with the @Serializable annotation, so that the kotlinx.serialization plugin can automatically generate a default serializer for it.

  • The @SerialName annotation allows redefining field names, making it possible to declare properties in data classes with more readable names.

Connect HTTP client

  1. In Greeting.kt, create a Ktor HTTPClient instance to execute network requests and parse the resulting JSON:

    import io.ktor.client.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json class Greeting { private val httpClient = HttpClient { install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true }) } } }

    To deserialize the result of the GET request, the ContentNegotiation Ktor plugin and the JSON serializer are used.

  2. In the greeting() function, retrieve the information about rocket launches by calling the httpClient.get() method and find the latest one:

    class Greeting { // ... @Throws(Exception::class) suspend fun greeting(): String { val rockets: List<RocketLaunch> = httpClient.get("https://api.spacexdata.com/v4/launches").body() val lastSuccessLaunch = rockets.last { it.launchSuccess == true } return "Guess what it is! > ${Platform().platform.reversed()}!" + "\nThere are only ${daysUntilNewYear()} left until New Year! 🎅🏼 " + "\nThe last successful launch was ${lastSuccessLaunch.launchDateUTC} 🚀" } }

    The suspend modifier in the greeting() function is necessary because it now contains a call to get(). It's a suspend function that has an asynchronous operation to retrieve data over the internet and can only be called from within a coroutine or another suspend function. The network request will be executed in the HTTP client's thread pool.

Add internet access permission

To access the internet, the Android application needs appropriate permission. Since all network requests are made from the shared module, it makes sense to add the internet access permission to its manifest.

Update your shared/src/androidMain/AndroidManifest.xml file the following way:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jetbrains.simplelogin.kotlinmultiplatformsandbox" > <uses-permission android:name="android.permission.INTERNET"/> </manifest>

Update Android and iOS apps

You've already updated the API of our shared module by adding the suspend modifier to the greeting() function. Now you need to update native (iOS, Android) parts of the project, so they can properly handle the result of calling the greeting() function.

Android app

As both a shared module and an Android application are written in Kotlin, using shared code from Android is straightforward:

  1. Add kotlinx.coroutines library to the Android application by adding a line in the build.gradle.kts in the androidApp folder:

    dependencies { // .. implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2") }
  2. In androidApp/src/main, update the MainActivity class replacing previous implementation:

    import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { private val scope = MainScope() override fun onDestroy() { super.onDestroy() scope.cancel() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tv: TextView = findViewById(R.id.text_view) tv.text = "Loading..." scope.launch { kotlin.runCatching { Greeting().greeting() }.onSuccess { tv.text = it }.onFailure { tv.text = it.localizedMessage } } } }

    The greeting() function is now called inside the coroutine launched in the main CoroutineScope.

iOS app

For the iOS part of the project, you'll make use of SwiftUI to build the user interface and the Model–view–viewmodel pattern to connect the UI to the shared module, which contains all the business logic.

The module is already connected to the iOS project — the Android Studio plugin wizard did all the configuration. The module is already imported and used in ContentView.swift with import shared.

  1. Launch your Xcode app and select Open a project or file.

  2. Navigate to your project, for example KotlinMultiplatformSandbox, and select the iosApp folder. Click Open.

  3. In iosApp/iosApp.swift, update the entry point for your app:

    struct iOSApp: App { var body: some Scene { WindowGroup { ContentView(viewModel: ContentView.ViewModel()) } } }
  4. In iosApp/ContentView.swift, create a ViewModel class for ContentView, which will prepare and manage data for it:

    import SwiftUI import shared struct ContentView: View { @ObservedObject private(set) var viewModel: ViewModel var body: some View { Text(viewModel.text) } } extension ContentView { class ViewModel: ObservableObject { @Published var text = "Loading..." init() { // Data will be loaded here } } }
    • ViewModel is declared as an extension to ContentView, as they are closely connected.

    • The Combine framework connects the view model (ContentView.ViewModel) with the view (ContentView).

    • ContentView.ViewModel is declared as an ObservableObject.

    • @Published wrapper is used for the text property.

    • @ObservedObject property wrapper is used to subscribe to the view model.

    Now the view model will emit signals whenever this property changes.

  5. Call the greeting() function, which now also loads data from the SpaceX API, and save the result in the text property:

    class ViewModel: ObservableObject { @Published var text = "Loading..." init() { Greeting().greeting { greeting, error in DispatchQueue.main.async { if let greeting = greeting { self.text = greeting } else { self.text = error?.localizedDescription ?? "error" } } } } }
    • Kotlin/Native provides bidirectional interoperability with Objective-C, thus Kotlin concepts, including suspend functions, are mapped to appropriate Swift/Objective-C and vice versa. When you compile a Kotlin module into an Apple framework, suspending functions are available in it as functions with callbacks (completionHandler).

    • The greeting() function was marked with the @Throws(Exception::class) annotation. So any exceptions that are instances of the Exception class or its subclass will be propagated as NSError, so you can handle them in the completionHandler.

    • When calling Kotlin suspend functions from Swift, completion handlers might be called on threads other than main, see the new memory manager migration guide. That's why DispatchQueue.main.async is used to update text property.

  6. Run both the iOS and Android applications from Android Studio and make sure your app's logic is synced:

    Final results

Next step

Now it's time to wrap up your project and see what's next.

See also

Get help

Last modified: 12 August 2022