diff --git a/images/multiplatform-create-app/first-multiplatform-project-3.png b/images/multiplatform-create-app/first-multiplatform-project-3.png
index c7e33c8b..59a49430 100644
Binary files a/images/multiplatform-create-app/first-multiplatform-project-3.png and b/images/multiplatform-create-app/first-multiplatform-project-3.png differ
diff --git a/images/multiplatform-create-app/multiplatform-mobile-upgrade-android.png b/images/multiplatform-create-app/multiplatform-mobile-upgrade-android.png
index a21bdbdf..c6f4e46d 100644
Binary files a/images/multiplatform-create-app/multiplatform-mobile-upgrade-android.png and b/images/multiplatform-create-app/multiplatform-mobile-upgrade-android.png differ
diff --git a/topics/multiplatform-onboard/multiplatform-create-first-app.md b/topics/multiplatform-onboard/multiplatform-create-first-app.md
index c5ad11a6..d4d98054 100644
--- a/topics/multiplatform-onboard/multiplatform-create-first-app.md
+++ b/topics/multiplatform-onboard/multiplatform-create-first-app.md
@@ -34,6 +34,7 @@ You can share application logic between iOS and Android apps and write platform-
* **Artifact**: greetingkmp
{width=800}
+ TODO update the screenshot
5. Select **Android** and **iOS** targets.
6. For iOS, select the **Do not share UI** option to keep the UI native.
@@ -49,26 +50,32 @@ You can share application logic between iOS and Android apps and write platform-
In IntelliJ IDEA, expand the `GreetingKMP` folder.
-This Kotlin Multiplatform project includes three modules:
+This Kotlin Multiplatform project includes the following modules:
-* _shared_ is a Kotlin module that contains the logic common for both Android and iOS applications – the code you share
- between platforms. It uses [Gradle](https://kotlinlang.org/docs/gradle.html) as the build system to help automate your build process.
-* _composeApp_ is a Kotlin module that builds into an Android application. It uses Gradle as the build system.
+* _sharedLogic_ is the multiplatform module that contains the logic common for both Android and iOS applications.
+* _sharedUI_ is the module with Compose Multiplatform UI code: in this project, it is used only by the Android app,
+ but it is a multiplatform module that can be used by other targets whenever you are ready for that.
+* _androidApp_ is a Kotlin module that builds into an Android application. It uses Gradle as the build system.
The composeApp module depends on and uses the shared module as a regular Android library.
-* _iosApp_ is an Xcode project that builds into an iOS application. It depends on and uses the shared module as an iOS
- framework. The shared module can be used as a regular framework or as a [CocoaPods dependency](multiplatform-cocoapods-overview.md).
- By default, Kotlin Multiplatform projects created in IntelliJ IDEA use the regular framework dependency.
+* _iosApp_ is an Xcode project that builds into an iOS application.
+ It depends on and uses the `sharedLogic` module as an iOS framework.
+ By default, Kotlin Multiplatform projects created using the IDE wizard use the regular framework dependency through
+ [direct integration](multiplatform-direct-integration.md).
+
+Every module except for `iosApp` uses Gradle as the build system.
{width=700}
-The shared module consists of three source sets: `androidMain`, `commonMain`, and `iosMain`. _Source set_ is a Gradle
-concept for a number of files logically grouped together where each group has its own dependencies.
+The shared module consists of three source sets:
+`androidMain`, `commonMain`, and `iosMain`.
+_Source set_ is a Gradle concept for a set of files logically grouped together where each set has its own dependencies.
In Kotlin Multiplatform, different source sets in a shared module can target different platforms.
-The common source set contains shared Kotlin code, and platform source sets use Kotlin code specific to each target.
+The `commonMain` source set contains shared Kotlin code, and platform source sets use Kotlin code specific to each target.
Kotlin/JVM is used for `androidMain` and Kotlin/Native for `iosMain`:
{width=350}
+TODO screenshot is outdated
When the shared module is built into an Android library, common Kotlin code is treated as Kotlin/JVM.
When it is built into an iOS framework, common Kotlin is treated as Kotlin/Native:
@@ -81,50 +88,36 @@ The common source set contains shared code that can be used across multiple targ
It's designed to contain code that is platform-independent. If you try to use platform-specific APIs in the common source set,
the IDE will show a warning:
-1. Open the `shared/src/commonMain/.../Greeting.kt` file
- where you can find an automatically generated `Greeting` class with a `greet()` function:
-
- ```kotlin
- class Greeting {
- private val platform = getPlatform()
-
- fun greet(): String {
- return "Hello, ${platform.name}!"
- }
- }
- ```
+1. Open the `sharedLogic/src/commonMain/.../Greeting.kt` file
+ where you can find the automatically generated `Greeting` class with a `greet()` function.
+2. Let's add a bit of variety to the greeting. Navigate to the definition of the `sayHello()` function in the `GreetingUtil.kt` file.
-2. Let's add a bit of variety to the greeting.
- Update the shared code with randomization and the `reversed()` call from the Kotlin standard library to reverse the text:
+3. Update the shared code with randomization and the `reversed()` call from the Kotlin standard library to reverse
+ the received string:
```kotlin
- class Greeting {
- private val platform: Platform = getPlatform()
+ fun sayHello(to: String): String {
+ val firstWord = if (Random.nextBoolean()) "Hi!" else "Hello!"
- fun greet(): String {
- //
- val firstWord = if (Random.nextBoolean()) "Hi!" else "Hello!"
-
- return "$firstWord Guess what this is! > ${platform.name.reversed()}!"
- }
+ return "$firstWord Guess what this is! > ${to.reversed()}!"
}
```
-3. Import the `kotlin.random.Random` class following the IDE's suggestion.
+4. Import the `kotlin.random.Random` class following the IDE's suggestion.
Writing the code only in common Kotlin has obvious limitations because it can't use any platform-specific functionality.
-Using interfaces and the [expect/actual](multiplatform-connect-to-apis.md) mechanism solves this.
+Using common interfaces with platform-specific implementations using the [expect/actual](multiplatform-connect-to-apis.md) mechanism solves this.
### Check out platform-specific implementations
-The common source set can define expected declarations (interfaces, classes, and so on).
-Then each platform source set, in this case `androidMain` and `iosMain`,
-has to provide actual platform-specific implementations for the expected declarations.
+The common source set can define expected declarations — interfaces, classes, and so on.
+In each platform source set, in this case `androidMain` and `iosMain`,
+you have to provide actual platform-specific implementations for the expected declarations.
While generating the code for a specific platform, the Kotlin compiler merges expected and actual declarations
and generates a single declaration with actual implementations.
1. When creating a Kotlin Multiplatform project with IntelliJ IDEA,
- you get a template with the `Platform.kt` file in the `commonMain` module:
+ you get a `Platform.kt` file in the `commonMain` module:
```kotlin
interface Platform {
@@ -132,13 +125,12 @@ and generates a single declaration with actual implementations.
}
```
- It's a common `Platform` interface with information about the platform.
+ It's a common `Platform` interface supposed to include information about a platform.
-2. Switch between the `androidMain` and the `iosMain` modules.
- You'll see that they have different implementations of the same functionality for the Android and the iOS source sets:
+2. You can find the platform-specific classes that implement the interface in the `androidMain` and the `iosMain` source sets:
```kotlin
- // Platform.android.kt in the androidMain module:
+ // Platform.android.kt in the androidMain source set
import android.os.Build
class AndroidPlatform : Platform {
@@ -147,7 +139,7 @@ and generates a single declaration with actual implementations.
```
```kotlin
- // Platform.ios.kt in the iosMain module:
+ // Platform.ios.kt in the iosMain source set
import platform.UIKit.UIDevice
class IOSPlatform: Platform {
@@ -156,14 +148,15 @@ and generates a single declaration with actual implementations.
}
```
- * The `name` property implementation from `AndroidPlatform` uses the Android-specific code, namely the `android.os.Build`
- dependency. This code is written in Kotlin/JVM. If you try to access a JVM-specific class, such as `java.util.Random` here, this code will compile.
- * The `name` property implementation from `IOSPlatform` uses iOS-specific code, namely the `platform.UIKit.UIDevice`
- dependency. It's written in Kotlin/Native, meaning you can write iOS code in Kotlin. This code becomes a part of the iOS
- framework, which you will later call from Swift in your iOS application.
+ * The `name` property of the `AndroidPlatform` class uses the Android-specific code, namely the `android.os.Build`
+ dependency. This code is interpreted as Kotlin/JVM.
+ If you try to access a JVM-specific class, such as `java.util.Random` here, this code will compile.
+ * The `name` property of the `IOSPlatform` class uses iOS-specific code, namely the `platform.UIKit.UIDevice`
+ dependency. This code is interpreted as Kotlin/Native, meaning that you can refer to iOS declarations in Kotlin.
+ The code becomes a part of the iOS framework, which is imported in the Swift code of the `iosApp` module.
-3. Check the `getPlatform()` function in different source sets. Its expected declaration doesn't have a body,
- and actual implementations are provided in the platform code:
+3. Each source set includes a `getPlatform()` function.
+ Its expected declaration doesn't have a body, and actual implementations are provided in the platform code:
```kotlin
// Platform.kt in the commonMain source set
@@ -184,15 +177,15 @@ Here, the common source set defines an expected `getPlatform()` function and has
`AndroidPlatform()` for the Android app and `IOSPlatform()` for the iOS app, in the platform source sets.
While generating the code for a specific platform, the Kotlin compiler merges expected and actual declarations
-into a single `getPlatform()` function with its actual implementations.
+into a single `getPlatform()` function with the correct implementation.
That's why expected and actual declarations should be defined in the same package – they are merged into one declaration
in the resulting platform code. Any invocation of the expected `getPlatform()` function in the generated platform code
-calls a correct actual implementation.
+should refer to the correct actual implementation.
Now you can run the apps and see all of this in action.
-#### Explore the expect/actual mechanism (optional) {initial-collapse-state="collapsed" collapsible="true"}
+#### Create an expect/actual variable (optional) {initial-collapse-state="collapsed" collapsible="true"}
The template project uses the expect/actual mechanism for functions, but it also works for most Kotlin declarations,
such as properties and classes. Let's implement an expected property:
@@ -213,40 +206,39 @@ such as properties and classes. Let's implement an expected property:
You'll get an error saying that expected declarations must not have a body, in this case an initializer.
The implementations must be provided in actual platform modules. Remove the initializer.
-3. Hover the `num` property and click **Create missed actuals...**.
- Choose the `androidMain` source set. You can then complete the implementation in `androidMain/Platform.android.kt`:
+3. Hover over the `expect` keyword for the `num` property and click **Create missed actuals...** in the IDE quick fix suggestion.
+4. Choose the `androidMain` source set. Complete the implementation in `androidMain/Platform.android.kt` to be as follows:
```kotlin
actual val num: Int = 1
```
-4. Now provide the implementation for the `iosMain` module. Add the following to `iosMain/Platform.ios.kt`:
+5. Now provide the actual implementation for `num` in the `iosMain` module. Add the following to the `iosMain/Platform.ios.kt` file:
```kotlin
actual val num: Int = 2
```
-5. In the `commonMain/Greeting.kt` file, add the `num` property to the `greet()` function to see the differences:
+6. In the `commonMain/GreetingUtil.kt` file, add the `num` property to the string formed by the `sayHello()` function:
- ```kotlin
- fun greet(): String {
- val firstWord = if (Random.nextBoolean()) "Hi!" else "Hello!"
-
- return "$firstWord [$num] Guess what this is! > ${platform.name.reversed()}!"
- }
- ```
+ ```kotlin
+ fun sayHello(to: String): String {
+ val firstWord = if (Random.nextBoolean()) "Hi!" else "Hello!"
+
+ return "$firstWord [$num] Guess what this is! > ${to.reversed()}!"
+ }
+ ```
## Run your application
-You can run your multiplatform application for both [Android](#run-your-application-on-android)
-or [iOS](#run-your-application-on-ios) from IntelliJ IDEA.
+You can run your multiplatform application for both Android and iOS from IntelliJ IDEA.
-If you have explored the expect/actual mechanism earlier, you can see "[1]" added to the greeting for Android
+If you have created the optional expect/actual variable earlier, you should see "[1]" added to the greeting for Android
and "[2]" added for iOS.
### Run your application on Android
-1. In the list of run configurations, select **composeApp**.
+1. In the list of run configurations, select **androidApp**.
2. Choose an Android virtual device next to the list of configurations and click **Run**.
If you don't have a device in the list, create a [new Android virtual device](https://developer.android.com/studio/run/managing-avds#createavd).
diff --git a/topics/multiplatform-onboard/multiplatform-dependencies.md b/topics/multiplatform-onboard/multiplatform-dependencies.md
index b0ab4899..57b3db47 100644
--- a/topics/multiplatform-onboard/multiplatform-dependencies.md
+++ b/topics/multiplatform-onboard/multiplatform-dependencies.md
@@ -15,8 +15,8 @@
-You've already created your first cross-platform Kotlin Multiplatform project! Now let's learn how to add dependencies
-to third-party libraries, which is necessary for building successful cross-platform applications.
+You've created and tweaked your first Kotlin Multiplatform project!
+Now let's learn how to add dependencies to third-party libraries, which is necessary for building successful cross-platform applications.
## Dependency types
@@ -26,14 +26,16 @@ There are two types of dependencies that you can use in Kotlin Multiplatform pro
common source set, `commonMain`.
Many modern Android libraries already have multiplatform support, like [Koin](https://insert-koin.io/),
- [Apollo](https://www.apollographql.com/), and [Okio](https://square.github.io/okio/). Find more multiplatform libraries on [klibs.io](https://klibs.io/),
- an experimental search service from JetBrains for discovering Kotlin Multiplatform libraries.
+ [Coil](https://coil-kt.github.io/coil/), and [SQLDelight](https://sqldelight.github.io/sqldelight/latest/).
+ Find more multiplatform libraries on [klibs.io](https://klibs.io/),
+ a search service offered by JetBrains for discovering Kotlin Multiplatform libraries.
-* _Native dependencies_. These are regular libraries from relevant ecosystems. In native projects you usually work with them
- using Gradle for Android and using CocoaPods or another dependency manager for iOS.
+* _Native dependencies_. These are regular libraries from specific ecosystems.
+ In native projects you usually work with them, for example, using Gradle for Android and Swift Package Manager for iOS.
- When you work with a shared module, typically, you still need native dependencies when you want to use platform APIs
- such as security storage. You can add native dependencies to the native source sets, `androidMain` and `iosMain`.
+ When you work with a multiplatform project module, typically, you still need native dependencies to use platform APIs
+ such as security storage, specific system calls, and so on.
+ In the build script, you specify native dependencies in the configuration of native source sets, for example, `androidMain` and `iosMain`.
For both types of dependencies, you can use local and external repositories.
@@ -44,17 +46,20 @@ For both types of dependencies, you can use local and external repositories.
>
{style="tip"}
-Let's go back to the app and make the greeting a little more festive. In addition to the device information, add a
-function to display the number of days left until New Year's Day. The `kotlinx-datetime` library, which has full
-multiplatform support, is the most convenient way to work with dates in your shared code.
+Let's go back to the app and make the greeting a little more festive:
+In addition to the OS version, add a function to display the number of days left until New Year's Day.
+The `kotlinx-datetime` library, which has full multiplatform support, is the most convenient way to work with dates in your shared code.
-1. Open the `build.gradle.kts` file located in the `shared` directory.
+1. Open the `build.gradle.kts` file located in the `sharedLogic` directory.
2. Add the following dependency and the Kotlin time opt-in to the `commonMain` source set dependencies:
```kotlin
kotlin {
//...
sourceSets {
+ // Opt-in necessary to use experimental kotlin.time APIs
+ // from the standard Kotlin library,
+ // distinct from the kotlinx-datetime library.
all { languageSettings.optIn("kotlin.time.ExperimentalTime") }
commonMain.dependencies {
@@ -66,12 +71,11 @@ multiplatform support, is the most convenient way to work with dates in your sha
3. Select the **Build | Sync Project with Gradle Files** menu item
or click the **Sync Gradle Changes** button in the build script editor to synchronize Gradle files: {width=50}
-4. Right-click the `shared/src/commonMain/.../greetingkmp` directory and select **New | Kotlin Class/File** to create a new file, `NewYear.kt`.
-5. Update the file with a short function that calculates
- the number of days from today until the New Year using the `datetime` date arithmetic:
+4. Right-click the `sharedLogic/src/commonMain/.../greeting` directory and select **New | Kotlin Class/File** to create a new file, `NewYear.kt`.
+5. In that file, add two functions that calculate
+ the number of days from today until the start of next year using the `datetime` date arithmetic and form the phrase to be displayed:
```kotlin
- @OptIn(ExperimentalTime::class)
fun daysUntilNewYear(): Int {
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
val closestNewYear = LocalDate(today.year + 1, 1, 1)
@@ -95,7 +99,7 @@ multiplatform support, is the most convenient way to work with dates in your sha
}
```
-8. To see the results, re-run your **composeApp** and **iosApp** configurations from IntelliJ IDEA:
+8. To see the results, re-run your **androidApp** and **iosApp** run configurations from IntelliJ IDEA:
{width=500}
diff --git a/topics/multiplatform-onboard/multiplatform-update-ui.md b/topics/multiplatform-onboard/multiplatform-update-ui.md
index 57258187..4b021d11 100644
--- a/topics/multiplatform-onboard/multiplatform-update-ui.md
+++ b/topics/multiplatform-onboard/multiplatform-update-ui.md
@@ -22,14 +22,14 @@ you store the data in the `phrases` variable and later iterate over it to produc
## Update the Android part
-The `composeApp` module contains an Android application, defines its main activity and the UI views, and uses the
-`shared` module as a regular Android library. The UI of the application uses the Compose Multiplatform framework.
+The `androidApp` module contains an Android application and defines its main activity.
+The Compose Multiplatform UI code is implemented in the `sharedUI` module, which the Android app uses as an Android library.
+The UI of the application uses the Compose Multiplatform framework.
Make some changes and see how they are reflected in the UI:
-1. Navigate to the `App.kt` file in the `composeApp/src/androidMain/.../greetingkmp` directory.
+1. Navigate to the `App.kt` file in the `sharedUI/src/commonMain/.../greeting` directory. TODO make sure that directory names are correct
2. Find the `Greeting` class invocation. Select the `greet()` function, right-click it, and select **Go To** | **Declaration or Usages**.
- You'll see that it's the same class from the `shared` module you edited in the previous step.
3. In the `Greeting.kt` file, update the `Greeting` class so that the `greet()` function returns a list of strings:
```kotlin
@@ -43,8 +43,8 @@ Make some changes and see how they are reflected in the UI:
}
}
```
-
-4. Go back to the `App.kt` file and update the `App()` implementation:
+4. Import the `kotlin.random.Random` package as the IDE suggests.
+5. Go back to the `App.kt` file and update the `App()` implementation to display the list of strings:
```kotlin
@Composable
@@ -71,27 +71,25 @@ Make some changes and see how they are reflected in the UI:
Here the `Column` composable shows each of the `Text` items, adding padding around them and space between them.
-5. Follow IntelliJ IDEA's suggestions to import the missing dependencies.
-6. Now you can run the Android app to see how it displays the list of strings:
+6. Follow the IDE's suggestions to import the missing dependencies.
+7. Now you can run the Android app to see how it displays the list of strings:
{width=300}
## Work with the iOS module
-The `iosApp` directory builds into an iOS application. It depends on and uses the `shared` module as an iOS
-framework. The UI of the app is written in Swift.
+The `iosApp` directory builds into an iOS application.
+It depends on and uses the `sharedLogic` module as an iOS framework.
+The UI of the app is written in Swift.
-Implement the same changes as in the Android app:
+Implement the same changes as in the Android app to account for the update in common code:
1. In IntelliJ IDEA, find the `iosApp/iosApp` folder at the root of your project in the **Project** tool window.
-2. Open the `iosApp/ContentView.swift` file, right-click the `Greeting().greet()` call, and select **Go To** | **Definition**.
-
- You'll see the Objective-C declarations for the Kotlin functions defined in the `shared` module. Kotlin types are
- represented as Objective-C types when used from Objective-C/Swift. Here the `greet()` function
- returns `List` in Kotlin and is seen from Swift as returning `NSArray`. For more on type mappings,
- see [Interoperability with Swift/Objective-C](https://kotlinlang.org/docs/native-objc-interop.html).
-
-3. Update the SwiftUI code to display a list of items in the same way as in the Android app:
+2. Open the `iosApp/ContentView.swift` file, right-click the `Greeting().greet()` call, and select **Go To** | **Declaration or Usages**.
+ You can see that IDEA correctly matches the Swift call with the Kotlin declaration.
+3. Open the `ContentView.swift` file again.
+ To display a list of strings in the same way as in the Android app,
+ replace the code for the `ContentView` structure with the following:
```Swift
struct ContentView: View {
@@ -116,12 +114,12 @@ Implement the same changes as in the Android app:
### Xcode reports errors in the code calling the shared framework
-If you are using Xcode, your Xcode project may still be using an old version of the framework.
+If you work in Xcode, your Xcode project may be using an old version of the framework.
To resolve this, return to IntelliJ IDEA and rebuild the project or start the iOS run configuration.
### Xcode reports an error when importing the shared framework
-If you are using Xcode, it may need to clear cached binaries: try resetting the environment by choosing
+If you are using Xcode, you may need to clear cached binaries: try resetting the environment by choosing
**Product | Clean Build Folder** in the main menu.
## Next step
diff --git a/topics/multiplatform-onboard/multiplatform-upgrade-app.md b/topics/multiplatform-onboard/multiplatform-upgrade-app.md
index 920c5530..3d0d06b0 100644
--- a/topics/multiplatform-onboard/multiplatform-upgrade-app.md
+++ b/topics/multiplatform-onboard/multiplatform-upgrade-app.md
@@ -20,8 +20,8 @@ requests and data serialization are the [most popular use cases](https://kotlinl
Multiplatform. Learn how to implement these 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 the [SpaceX API](https://github.com/r-spacex/SpaceX-API/tree/master/docs#rspacex-api-docs)
-and display the date of the last successful launch of a SpaceX rocket.
+The updated app will retrieve data over the internet from the [LaunchLibrary 2](https://lldev.thespacedevs.com/docs)
+API and display the date of the last successful launch of a SpaceX rocket.
> You can find the final state of the project in two branches of our GitHub repository, with different coroutine solutions:
> * the [`main`](https://github.com/kotlin-hands-on/get-started-with-kmp/tree/main) branch includes a KMP-NativeCoroutines implementation,
@@ -33,16 +33,15 @@ and display the date of the last successful launch of a SpaceX rocket.
You'll need to add the following multiplatform libraries in your project:
-* [`kotlinx.coroutines`](https://github.com/Kotlin/kotlinx.coroutines), to use coroutines for asynchronous code,
- which allows simultaneous operations.
-* [`kotlinx.serialization`](https://github.com/Kotlin/kotlinx.serialization), to deserialize JSON responses into objects of entity classes used to process
+* [`kotlinx.coroutines`](https://github.com/Kotlin/kotlinx.coroutines), to use coroutines for simultaneous operations.
+* [`kotlinx.serialization`](https://github.com/Kotlin/kotlinx.serialization), to deserialize JSON responses of the SpaceX API into objects of entity classes used to process
network operations.
-* [Ktor](https://ktor.io/), a framework to create an HTTP client for retrieving data over the internet.
+* [Ktor](https://ktor.io/), a framework for sending and retrieving data over HTTP.
### kotlinx.coroutines
To add `kotlinx.coroutines` to your project, specify a dependency in the common source set. To do so, add the following
-line to the `shared/build.gradle.kts` file:
+line to the `sharedLogic/build.gradle.kts` file:
```kotlin
kotlin {
@@ -56,13 +55,13 @@ kotlin {
}
```
-The Kotlin Multiplatform Gradle plugin automatically adds a dependency to the platform-specific (iOS and Android) parts
+The Kotlin Multiplatform Gradle plugin automatically adds a dependency on the platform-specific (iOS and Android) artifacts
of `kotlinx.coroutines`.
### kotlinx.serialization
To use the `kotlinx.serialization` library, set up a corresponding Gradle plugin.
-To do that, add the following line to the existing `plugins {}` block at the very beginning of the `shared/build.gradle.kts` file:
+To do that, add the following line to the existing `plugins {}` block at the very beginning of the `sharedLogic/build.gradle.kts` file:
```kotlin
plugins {
@@ -73,10 +72,10 @@ plugins {
### Ktor
-You need to add the core dependency (`ktor-client-core`) to the common source set of the shared module.
-You also need to add supporting dependencies:
+Add the base Ktor client dependency (`ktor-client-core`) to the common source set of the shared module,
+along with these supporting dependencies:
-* Add the `ContentNegotiation` functionality (`ktor-client-content-negotiation`), which allows serializing and deserializing
+* Add the `ContentNegotiation` library (`ktor-client-content-negotiation`), which allows serializing and 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 serialization library. Ktor will expect JSON data and deserialize it into a data class when receiving responses.
@@ -108,14 +107,14 @@ kotlin {
Synchronize the Gradle files by clicking the **Sync Gradle Changes** button.
-## Create API requests
+## Set up API requests
-You'll need the [SpaceX API](https://github.com/r-spacex/SpaceX-API/tree/master/docs#rspacex-api-docs) to retrieve data, and you'll use a single method to
-get the list of all launches from the **v4/launches** endpoint.
+You'll use the [Launch Library API](https://github.com/r-spacex/SpaceX-API/tree/master/docs#rspacex-api-docs) to retrieve data,
+specifically the list of all launches from the **/2.3.0/launches** endpoint.
-### Add a data model
+### Create a data model
-In the `shared/src/commonMain/.../greetingkmp` directory, create a new `RocketLaunch.kt` file
+In the `sharedLogic/src/commonMain/.../greeting` directory, create a new `RocketLaunch.kt` file
and add a data class which stores data from the SpaceX API:
```kotlin
@@ -123,26 +122,40 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
-data class RocketLaunch (
- @SerialName("flight_number")
- val flightNumber: Int,
+data class RocketLaunch(
+ @SerialName("id")
+ val id: String,
@SerialName("name")
val missionName: String,
- @SerialName("date_utc")
+ @SerialName("net")
val launchDateUTC: String,
- @SerialName("success")
- val launchSuccess: Boolean?,
+ @SerialName("status")
+ val status: LaunchStatus,
+)
+
+@Serializable
+data class LaunchStatus(
+ @SerialName("id")
+ val id: Int,
+ @SerialName("name")
+ val name: String,
+)
+
+@Serializable
+data class LaunchListResponse(
+ @SerialName("results")
+ val results: List,
)
```
-* The `RocketLaunch` class is marked with the `@Serializable` annotation, so that the `kotlinx.serialization` plugin can
+* 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 you to redefine field names, making it possible to declare properties in data classes
- with more readable names.
+* The `@SerialName` annotation allows you to redefine field names, making it possible to declare properties with more readable names
+ in data classes.
### Connect HTTP client
-1. In the `shared/src/commonMain/.../greetingkmp` directory, create a new `RocketComponent` class.
+1. In the `sharedLogic/src/commonMain/.../greeting` directory, create a new `RocketComponent` class.
2. Add the `httpClient` property to retrieve rocket launch information through an HTTP GET request:
```kotlin
@@ -165,28 +178,14 @@ data class RocketLaunch (
```
* The [`ContentNegotiation`](https://ktor.io/docs/serialization-client.html#register_json) Ktor plugin and the JSON serializer deserialize the result of the GET request.
- * The JSON serializer here is configured in a way that it prints JSON in a more readable manner with the `prettyPrint` property. It
- is more flexible when reading malformed JSON with `isLenient`,
+ * The JSON serializer here is configured in such a way that it prints JSON in a more readable manner with the `prettyPrint` property.
+ This is more flexible when reading malformed JSON with `isLenient`,
and it ignores keys that haven't been declared in the rocket launch model with `ignoreUnknownKeys`.
-3. Add the `getDateOfLastSuccessfulLaunch()` suspending function to `RocketComponent`:
+3. Add the `getDateOfLastSuccessfulLaunch()` [suspending function](https://kotlinlang.org/docs/coroutines-basics.html) to `RocketComponent`,
+ which will retrieve information about rocket launches asynchronously:
```kotlin
- class RocketComponent {
- // ...
-
- private suspend fun getDateOfLastSuccessfulLaunch(): String {
-
- }
- }
- ```
-
-4. Call the `httpClient.get()` function to retrieve information about rocket launches:
-
- ```kotlin
- import io.ktor.client.request.get
- import io.ktor.client.call.body
-
class RocketComponent {
// ...
@@ -198,25 +197,26 @@ data class RocketLaunch (
* `httpClient.get()` is also a suspending function
because it needs to retrieve data over the network asynchronously without blocking threads.
- * Suspending functions can only be called from coroutines or other suspending functions. This is why `getDateOfLastSuccessfulLaunch()`
- was marked with the `suspend` keyword. The network request is executed in the HTTP client's thread pool.
+ * Suspending functions can only be called from coroutines or other suspending functions.
+ This is why `getDateOfLastSuccessfulLaunch()` was marked with the `suspend` keyword.
+ The network request is executed in the HTTP client's thread pool.
-5. Update the function again to find the last successful launch in the list:
+4. After the HTTP request call, add the call to get the last successful launch in the list
+ (the list of launches is sorted by date from oldest to newest):
```kotlin
class RocketComponent {
// ...
private suspend fun getDateOfLastSuccessfulLaunch(): String {
- val rockets: List = httpClient.get("https://api.spacexdata.com/v4/launches").body()
- val lastSuccessLaunch = rockets.last { it.launchSuccess == true }
+ val response: LaunchListResponse =
+ httpClient.get("https://lldev.thespacedevs.com/2.3.0/launches/previous/?mode=list&limit=10&format=json").body()
+ val lastSuccessLaunch = response.results.first { it.status.id == 3 }
}
}
```
- The list of rocket launches is sorted by date from oldest to newest.
-
-6. Convert the launch date from UTC to your local date and format the output:
+5. Convert the launch date from UTC to your local date, then format the output:
```kotlin
import kotlinx.datetime.TimeZone
@@ -227,11 +227,10 @@ data class RocketLaunch (
class RocketComponent {
// ...
- @OptIn(ExperimentalTime::class)
private suspend fun getDateOfLastSuccessfulLaunch(): String {
- val rockets: List =
- httpClient.get("https://api.spacexdata.com/v4/launches").body()
- val lastSuccessLaunch = rockets.last { it.launchSuccess == true }
+ val response: LaunchListResponse =
+ httpClient.get("https://lldev.thespacedevs.com/2.3.0/launches/previous/?mode=list&limit=10&format=json").body()
+ val lastSuccessLaunch = response.results.first { it.status.id == 3 }
val date = Instant.parse(lastSuccessLaunch.launchDateUTC)
.toLocalDateTime(TimeZone.currentSystemDefault())
@@ -240,10 +239,10 @@ data class RocketLaunch (
}
```
- The date will be in the "MMMM DD, YYYY" format, for example, OCTOBER 5, 2022.
+ The date will be in the "MMMM DD, YYYY" format, for example, "OCTOBER 5, 2022".
-7. Add another suspending function, `launchPhrase()`, which will create a message using the `getDateOfLastSuccessfulLaunch()`
- function:
+6. To the same class, add another suspending function, `launchPhrase()`,
+ which will create a message using the `getDateOfLastSuccessfulLaunch()` function:
```kotlin
class RocketComponent {
@@ -259,10 +258,11 @@ data class RocketLaunch (
}
```
-### Create the flow
+### Create a coroutine flow
-You can use flows instead of suspending functions. They emit a sequence of values instead of a single value that
-suspending functions return.
+Instead of simply calling a suspending function, you can use [flows](https://kotlinlang.org/docs/flow.html)
+when you need to produce a sequence of values.
+Flows can emit a sequence of values as the values are produced instead of returning a single value like suspending functions.
1. Open the `Greeting.kt` file in the `shared/src/commonMain/kotlin` directory.
2. Add a `rocketComponent` property to the `Greeting` class. The property will store the message with the last successful launch date:
@@ -296,38 +296,24 @@ suspending functions return.
* The `Flow` emits strings with a delay of one second between each emission. The last element is only emitted after
the network response returns, so the exact delay depends on your network.
-### Add internet access permission
-
-To access the internet, the Android application needs the 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 `composeApp/src/androidMain/AndroidManifest.xml` file with the access permission:
-
-```xml
-
-
-
- ...
-
-```
-
-You've already updated the API of the shared module by changing the return type of the `greet()` function to `Flow`.
+You've updated the API of the shared module by changing the return type of the `greet()` function to `Flow`.
Now you need to update native parts of the project so that they can properly handle the result of calling
the `greet()` function.
-
## Update native Android UI
As both the shared module and the Android application are written in Kotlin, using shared code from Android is straightforward.
### Introduce a view model
-Now that the application is becoming more complex, it's time to introduce a view model to the [Android activity](https://developer.android.com/guide/components/activities/intro-activities)
-called `MainActivity`. It invokes the `App()` function that implements the UI.
-The view model will manage the data from the activity and won't disappear when the activity undergoes a lifecycle change.
+View models are a popular pattern in Android development that helps manage data and other app components that should
+persist through [Android activity](https://developer.android.com/guide/components/activities/intro-activities) lifecycle.
+Now that the application is becoming more complex, it's time to introduce a view model into our app as well.
+It will store the data received from the SpaceX API and make it available to the UI.
+
+Create the view model class in the Android platform code:
-1. In the `composeApp/src/androidMain/.../greetingkmp` directory,
- create a new `MainViewModel` Kotlin class:
+1. In the `sharedUI/src/commonMain/.../greetingkmp` directory, create a new `MainViewModel` Kotlin class:
```kotlin
import androidx.lifecycle.ViewModel
@@ -337,7 +323,7 @@ The view model will manage the data from the activity and won't disappear when t
}
```
- This class extends Android's `ViewModel` class, which ensures the correct behavior regarding lifecycle and configuration changes.
+ This class extends Android's `ViewModel` class to align with the platform's expectations regarding lifecycle and configuration changes.
2. Create a `greetingList` value of the [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)
type and its backing property:
@@ -355,7 +341,7 @@ The view model will manage the data from the activity and won't disappear when t
* `StateFlow` here extends the `Flow` interface but has a single value or state.
* The private backing property `_greetingList` ensures that only clients of this class can access the read-only `greetingList` property.
-3. In the `init` function of the View Model, collect all the strings from the `Greeting().greet()` flow:
+3. In the `init` function of the view model, collect all the strings from the `Greeting().greet()` flow:
```kotlin
import androidx.lifecycle.viewModelScope
@@ -375,10 +361,11 @@ The view model will manage the data from the activity and won't disappear when t
}
```
- Since the `collect()` function is suspended, the `launch` coroutine is used within the view model's scope.
+ Since the `Flow.collect()` function is suspending, the `launch` coroutine is used within the view model's scope.
This means that the launch coroutine will run only during the correct phases of the view model's lifecycle.
-4. Inside the `collect` trailing lambda, update the value of `_greetingList` to append the collected `phrase` to the list of phrases in `list`:
+4. Inside the `collect` trailing lambda, append the collected `phrase` to the list of phrases in `_greetingList` using
+ the `update()` function:
```kotlin
import kotlinx.coroutines.flow.update
@@ -396,11 +383,10 @@ The view model will manage the data from the activity and won't disappear when t
}
```
- The `update()` function will update the value automatically.
-
### Use the view model's flow
-1. In `composeApp/src/androidMain/kotlin`, open the `App.kt` file and update it, replacing the previous implementation:
+1. In `sharedUI/src/commonMain/.../greetingkmp`, open the `App.kt` file and update it,
+ replacing the previous implementation to use the newly implemented view model:
```kotlin
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -430,19 +416,36 @@ The view model will manage the data from the activity and won't disappear when t
* The `collectAsStateWithLifecycle()` function calls on `greetingList` to collect the value from the ViewModel's flow
and represent it as a composable state in a lifecycle-aware manner.
- * When a new flow is created, the compose state will change and display a scrollable `Column` with greeting phrases
+ * When a new flow is created, the composition state will change and display a scrollable `Column` with greeting phrases
arranged vertically and separated by dividers.
-2. To see the results, rerun your **composeApp** configuration:
+### Add internet access permission
+
+To access the internet, the Android application needs the appropriate permission. Since all network requests are made from the
+shared module, it makes sense to add the internet access permission to its manifest.
- {width=300}
+Update your `androidApp/src/main/AndroidManifest.xml` file with the access permission:
+
+```xml
+
+
+
+ ...
+
+```
+
+### Run the app
+
+To see the final result, rerun your **androidApp** run configuration:
+
+{width=300}
## Update native iOS UI
For the iOS part of the project, you'll make use of the [Model–view–viewmodel](https://en.wikipedia.org/wiki/Model–view–viewmodel)
-pattern again to connect the UI to the shared module, which contains all the business logic.
+pattern, like you did for the Android app, to connect the UI to the `sharedLogic` module.
-The module is already imported in the `ContentView.swift` file with the `import Shared` declaration.
+The module is already imported in the `ContentView.swift` file with the `import SharedLogic` declaration.
### Introducing a ViewModel
@@ -451,7 +454,7 @@ Call the `startObserving()` function within a `task()` call to support concurren
```swift
import SwiftUI
-import Shared
+import SharedLogic
struct ContentView: View {
@ObservedObject private(set) var viewModel: ViewModel
@@ -486,12 +489,14 @@ struct ListView: View {
* `ViewModel` is declared as an extension to `ContentView`, as they are closely connected.
* `ViewModel` has a `greetings` property that is an array of `String` phrases.
- SwiftUI connects the ViewModel (`ContentView.ViewModel`) with the view (`ContentView`).
+
+SwiftUI connects the view model (`ContentView.ViewModel`) with the view (`ContentView`):
+
* `ContentView.ViewModel` is declared as an `ObservableObject`.
-* The `@Published` wrapper is used for the `greetings` property.
-* The `@ObservedObject` property wrapper is used to subscribe to the ViewModel.
+ The `@ObservedObject` wrapper for the `viewModel` property in `ContentView` subscribes the view to the view model.
+* The `greetings` property of the view model uses the `@Published` wrapper.
+ It allows SwiftUI to automatically update the view when this property changes.
-This ViewModel will emit signals whenever this property changes.
Now you need to implement the `startObserving()` function to consume flows.
### Choose a library to consume flows from iOS
@@ -523,22 +528,20 @@ wrappers.
```kotlin
plugins {
// ...
- id("com.google.devtools.ksp").version("%kspVersion%").apply(false)
id("com.rickclephas.kmp.nativecoroutines").version("%kmpncVersion%").apply(false)
}
```
-2. In the `shared/build.gradle.kts` file, add the KMP-NativeCoroutines plugin:
+2. In the `sharedLogic/build.gradle.kts` file, add the KMP-NativeCoroutines plugin:
```kotlin
plugins {
// ...
- id("com.google.devtools.ksp")
id("com.rickclephas.kmp.nativecoroutines")
}
```
-3. Also in the `shared/build.gradle.kts` file, opt-in to the experimental `@ObjCName` annotation:
+3. Also, in the `sharedLogic/build.gradle.kts` file, opt-in to the experimental `@ObjCName` annotation:
```kotlin
kotlin {
@@ -578,6 +581,8 @@ wrappers.
#### Import the library using SPM in XCode
+Installs the parts of the KMP-NativeCoroutines Swift package necessary to work with the `async/await` mechanism.
+
1. Go to **File** | **Open Project in Xcode**.
2. In Xcode, right-click the `iosApp` project in the left-hand menu and select **Add Package Dependencies**.
3. In the search bar, enter the package name:
@@ -594,8 +599,6 @@ wrappers.
{width=500}
-This should install the parts of the KMP-NativeCoroutines package necessary to work with the `async/await` mechanism.
-
#### Consume the flow using the KMP-NativeCoroutines library
1. In `iosApp/ContentView.swift`, update the `startObserving()` function to consume the flow using KMP-NativeCoroutine's
@@ -647,24 +650,25 @@ every time the flow emits a value.
### Option 2. Configure SKIE {initial-collapse-state="collapsed" collapsible="true"}
-To set up the library, specify the SKIE plugin in `shared/build.gradle.kts` and click the **Sync Gradle Changes** button.
+To set up the library, specify the SKIE plugin in `sharedLogic/build.gradle.kts` and click the **Sync Gradle Changes** button.
```kotlin
plugins {
id("co.touchlab.skie") version "%skieVersion%"
}
```
-
-> The 0.10.6 version of SKIE latest at the moment of writing does not support the latest Kotlin.
-> To use it, downgrade your Kotlin version to 2.2.10 in the `gradle/libs.versions.toml` file.
->
-{style="warning"}
+TODO make version catalogs examples
#### Consume the flow using SKIE
You'll use a loop and the `await` mechanism to iterate through the `Greeting().greet()` flow and update the `greetings`
property every time the flow emits a value.
+> IntelliJ IDEA and Android Studio can incorrectly report Swift errors in calls to Kotlin code while using SKIE.
+> This is a known issue with the library, which doesn't affect building and running the app.
+>
+{style="warning"}
+
Make sure `ViewModel` is marked with the `@MainActor` annotation.
The annotation ensures that all asynchronous operations within `ViewModel` run on the main thread
to comply with the Kotlin/Native requirement:
diff --git a/v.list b/v.list
index 08e947c4..05d906d8 100644
--- a/v.list
+++ b/v.list
@@ -41,8 +41,8 @@
-
-
+
+