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 operationskotlinx.serialization
for deserializing JSON responses into objects of entity classes used to process network operationsKtor 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:
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:
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:
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 andkotlinx.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
).
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:
The
RocketLaunch
class is marked with the@Serializable
annotation, so that thekotlinx.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
In
Greeting.kt
, create a KtorHTTPClient
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.
In the
greeting()
function, retrieve the information about rocket launches by calling thehttpClient.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 thegreeting()
function is necessary because it now contains a call toget()
. 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:
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:
Add
kotlinx.coroutines
library to the Android application by adding a line in thebuild.gradle.kts
in theandroidApp
folder:dependencies { // .. implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2") }In
androidApp/src/main
, update theMainActivity
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 mainCoroutineScope
.
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
.
Launch your Xcode app and select Open a project or file.
Navigate to your project, for example KotlinMultiplatformSandbox, and select the
iosApp
folder. Click Open.In
iosApp/iosApp.swift
, update the entry point for your app:struct iOSApp: App { var body: some Scene { WindowGroup { ContentView(viewModel: ContentView.ViewModel()) } } }In
iosApp/ContentView.swift
, create aViewModel
class forContentView
, 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 toContentView
, as they are closely connected.The Combine framework connects the view model (ContentView.ViewModel) with the view (ContentView).
ContentView.ViewModel
is declared as anObservableObject
.@Published
wrapper is used for thetext
property.@ObservedObject
property wrapper is used to subscribe to the view model.
Now the view model will emit signals whenever this property changes.
Call the
greeting()
function, which now also loads data from the SpaceX API, and save the result in thetext
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 theException
class or its subclass will be propagated asNSError
, so you can handle them in thecompletionHandler
.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 whyDispatchQueue.main.async
is used to updatetext
property.
Run both the iOS and Android applications from Android Studio and make sure your app's logic is synced:
Next step
Now it's time to wrap up your project and see what's next.
See also
Explore various approaches to composition of suspending functions.
Learn more about the interoperability with Objective-C frameworks and libraries.
Complete this tutorial on networking and data storage.
Get help
Kotlin Slack. Get an invite and join the #multiplatform channel.
Kotlin issue tracker. Report a new issue.