diff --git a/images/ktor-sql/android-application.png b/images/ktor-sql/android-application.png index 68282d33..5ecf017c 100644 Binary files a/images/ktor-sql/android-application.png and b/images/ktor-sql/android-application.png differ diff --git a/topics/development/multiplatform-ktor-sqldelight.md b/topics/development/multiplatform-ktor-sqldelight.md index 34f5a240..24b31430 100644 --- a/topics/development/multiplatform-ktor-sqldelight.md +++ b/topics/development/multiplatform-ktor-sqldelight.md @@ -40,7 +40,8 @@ You will use the following multiplatform libraries in the project: 1. In the [quickstart](quickstart.md), complete the instructions to [set up your environment for Kotlin Multiplatform development](quickstart.md#set-up-the-environment). 2. In IntelliJ IDEA, select **File** | **New** | **Project**. -3. In the panel on the left, select **Kotlin Multiplatform**. +3. In the panel on the left, select **Kotlin Multiplatform** (in Android Studio, the template can be found in the **Generic** tab + of the **New Project** wizard). 4. Specify the following fields in the **New Project** window: * **Name**: SpaceTutorial @@ -66,15 +67,15 @@ Change or add lines in the version catalog in the `gradle/libs.versions.toml` fi ``` [versions] - agp = "8.6.0" + agp = "8.7.3" ... coroutinesVersion = "%coroutinesVersion%" - dateTimeVersion = "%dateTimeVersion%" + dateTimeVersion = "0.6.2" koin = "%koinVersion%" ktor = "%ktorVersion%" sqlDelight = "%sqlDelightVersion%" - lifecycleViewmodelCompose = "2.7.0" - material3 = "1.2.0" + lifecycleViewmodelCompose = "2.9.1" + material3 = "1.3.2" ``` {initial-collapse-state="collapsed" collapsible="true" collapsed-title="[versions]"} @@ -122,12 +123,16 @@ Change or add lines in the version catalog in the `gradle/libs.versions.toml` fi } ``` -6. In the same `shared/build.gradle.kts` file, refer to all the required dependencies: +6. The common source set requires a core artifact of each library, as well as the Ktor [serialization feature](https://ktor.io/docs/serialization-client.html) + to use `kotlinx.serialization` for processing network requests and responses. + The iOS and Android source sets also need SQLDelight and Ktor platform drivers. + + In the same `shared/build.gradle.kts` file, add all the required dependencies: ```kotlin kotlin { // ... - + sourceSets { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) @@ -150,11 +155,6 @@ Change or add lines in the version catalog in the `gradle/libs.versions.toml` fi } ``` - * The common source set requires a core artifact of each library, - as well as the Ktor [serialization feature](https://ktor.io/docs/serialization-client.html) - to use `kotlinx.serialization` for processing network requests and responses. - * The iOS and Android source sets also need SQLDelight and Ktor platform drivers. - 7. Once the dependencies are added, click the **Sync Gradle Changes** button to synchronize Gradle files once again. After the Gradle sync, you are done with the project configuration and can start writing code. @@ -180,7 +180,8 @@ The application data model will have three entity classes with: Create the necessary data classes: -1. In the `shared/src/commonMain/kotlin/com/jetbrains/spacetutorial` directory, create the `entity` directory with the `Entity.kt` file inside. +1. In the `shared/src/commonMain/kotlin/com/jetbrains/spacetutorial` directory, create the `entity` package, + then create the `Entity.kt` file inside that package. 2. Declare all the data classes for basic entities: ```kotlin @@ -295,8 +296,8 @@ While you can achieve this with [expected and actual interfaces](multiplatform-e in this project, you will use [Koin](https://insert-koin.io/) to try dependency injection in Kotlin Multiplatform. 1. Create an interface for database drivers. To do this, in the `shared/src/commonMain/kotlin/com/jetbrains/spacetutorial/` directory, - create the `cache` directory. -2. Create the `DatabaseDriverFactory` interface inside the `cache` directory: + create the `cache` package. +2. Create the `DatabaseDriverFactory` interface inside the `cache` package: ```kotlin package com.jetbrains.spacetutorial.cache @@ -309,7 +310,7 @@ in this project, you will use [Koin](https://insert-koin.io/) to try dependency ``` 3. Create the class implementing this interface for Android: in the `shared/src/androidMain/kotlin` directory, - create the `com.jetbrains.spacetutorial.cache` package with the `DatabaseDriverFactory.kt` file inside. + create the `com.jetbrains.spacetutorial.cache` package, then create the `DatabaseDriverFactory.kt` file inside it. 4. On Android, the SQLite driver is implemented by the `AndroidSqliteDriver` class. In the `DatabaseDriverFactory.kt` file, pass the database information and the context link to the `AndroidSqliteDriver` class constructor: @@ -327,8 +328,8 @@ in this project, you will use [Koin](https://insert-koin.io/) to try dependency } ``` -5. For iOS, in the `shared/src/iosMain/kotlin/com/jetbrains/spacetutorial/` directory, create a `cache` directory. -6. Inside the `cache` directory, create the `DatabaseDriverFactory.kt` file and add this code: +5. For iOS, in the `shared/src/iosMain/kotlin/com/jetbrains/spacetutorial/` directory, create the `cache` package. +6. Inside the `cache` package, create the `DatabaseDriverFactory.kt` file and add this code: ```kotlin package com.jetbrains.spacetutorial.cache @@ -351,7 +352,7 @@ So far, you have added factories for platform database drivers and an `AppDataba Now, create a `Database` class, which will wrap the `AppDatabase` interface and contain the caching logic. 1. In the common source set `shared/src/commonMain/kotlin`, create a new `Database` class in - the `com.jetbrains.spacetutorial.cache` directory. It will contain logic common to both platforms. + the `com.jetbrains.spacetutorial.cache` package. It will contain logic common to both platforms. 2. To provide a driver for `AppDatabase`, pass an abstract `DatabaseDriverFactory` instance to the `Database` class constructor: @@ -439,45 +440,50 @@ Now, create a `Database` class, which will wrap the `AppDatabase` interface and } ``` -## Implement an API service +## Implement the API service To retrieve data over the internet, you'll use the [SpaceX public API](https://github.com/r-spacex/SpaceX-API/tree/master/docs#rspacex-api-docs) and a single method to retrieve the list of all launches from the `v5/launches` endpoint. Create a class that will connect the application to the API: -1. In the common source set `shared/src/commonMain/kotlin`, create a directory with the name `com/jetbrains/spacetutorial/network`. +1. In the `shared/src/commonMain/kotlin/com/jetbrains/spacetutorial/` directory, create a `network` package. 2. Inside the `network` directory, create the `SpaceXApi` class: - ```kotlin - package com.jetbrains.spacetutorial.network - - import io.ktor.client.HttpClient - import io.ktor.client.plugins.contentnegotiation.ContentNegotiation - import io.ktor.serialization.kotlinx.json.json - import kotlinx.serialization.json.Json - - class SpaceXApi { - private val httpClient = HttpClient { - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - useAlternativeNames = false - }) - } - } - } - ``` + ```kotlin + package com.jetbrains.spacetutorial.network + + import io.ktor.client.HttpClient + import io.ktor.client.plugins.contentnegotiation.ContentNegotiation + import io.ktor.serialization.kotlinx.json.json + import kotlinx.serialization.json.Json + + class SpaceXApi { + private val httpClient = HttpClient { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + useAlternativeNames = false + }) + } + } + } + ``` - * This class executes network requests and deserializes JSON responses into entities from the `com.jetbrains.spacetutorial.entity` package. - The Ktor `HttpClient` instance initializes and stores the `httpClient` property. - * This code uses the [Ktor `ContentNegotiation` plugin](https://ktor.io/docs/serialization-client.html) - to deserialize the result of a `GET` request. The plugin processes the request and the response payload as JSON, - serializing and deserializing them as needed. + This class executes network requests and deserializes JSON responses into entities from the `com.jetbrains.spacetutorial.entity` package. + The Ktor `HttpClient` instance initializes and stores the `httpClient` property. + + This code uses the [Ktor `ContentNegotiation` plugin](https://ktor.io/docs/serialization-client.html) + to deserialize the result of a `GET` request. The plugin processes the request and the response payload as JSON, + serializing and deserializing them as needed. 3. Declare the data retrieval function that returns the list of rocket launches: ```kotlin + import com.jetbrains.spacetutorial.entity.RocketLaunch + import io.ktor.client.request.get + import io.ktor.client.call.body + class SpaceXApi { // ... @@ -487,17 +493,18 @@ Create a class that will connect the application to the API: } ``` - * The `getAllLaunches` function has the `suspend` modifier because it contains a call of the suspend function `HttpClient.get()`. +The `getAllLaunches` function has the `suspend` modifier because it contains a call of the suspend function `HttpClient.get()`. The `get()` function includes an asynchronous operation to retrieve data over the internet and can only be called from a coroutine or another suspend function. The network request will be executed in the HTTP client's thread pool. - * The URL for sending a GET request is passed as an argument to the `get()` function. + +The URL for sending a GET request is passed as an argument to the `get()` function. ## Build an SDK Your iOS and Android applications will communicate with the SpaceX API through the shared module, which will provide a public class, `SpaceXSDK`. -1. In the common source set `shared/src/commonMain/kotlin`, in the `com.jetbrains.spacetutorial` directory, create +1. In the common source set `shared/src/commonMain/kotlin`, in the `com.jetbrains.spacetutorial` package, create the `SpaceXSDK` class. This class will be the facade for the `Database` and `SpaceXApi` classes. @@ -539,12 +546,14 @@ public class, `SpaceXSDK`. } ``` -* The class contains one function for getting all launch information. Depending on the value of `forceReload`, it +The class contains one function for getting all launch information. Depending on the value of `forceReload`, it returns cached values or loads the data from the internet and then updates the cache with the results. If there is no cached data, it loads the data from the internet regardless of the `forceReload` flag's value. -* Clients of your SDK could use a `forceReload` flag to load the latest information about the launches, + +Clients of your SDK could use a `forceReload` flag to load the latest information about the launches, enabling the pull-to-refresh gesture for users. -* All Kotlin exceptions are unchecked, while Swift has only checked errors (see [Interoperability with Swift/Objective-C](https://kotlinlang.org/docs/native-objc-interop.html#errors-and-exceptions) + +All Kotlin exceptions are unchecked, while Swift has only checked errors (see [Interoperability with Swift/Objective-C](https://kotlinlang.org/docs/native-objc-interop.html#errors-and-exceptions) for details). Thus, to make your Swift code aware of expected exceptions, Kotlin functions called from Swift should be marked with the `@Throws` annotation specifying a list of potential exception classes. @@ -733,7 +742,7 @@ function that brings it all together. 4. Now, in the `AppModule.kt` file, specify the view model in the Koin module: ```kotlin - import org.koin.androidx.viewmodel.dsl.viewModel + import org.koin.core.module.dsl.viewModel val appModule = module { // ... @@ -755,8 +764,7 @@ You will build your main `App()` composable around the `AppTheme` function suppl ![theme directory location](theme-directory.png){width=299} -4. In each theme file, `Color.kt` and `Theme.kt`, make sure that the package line refers - to your package: +4. In each file inside the `theme` package, change the `package` line to refer to the package you created: ```kotlin package com.jetbrains.spacetutorial.theme @@ -778,26 +786,29 @@ Create the main `App()` composable for your application, and call it from a `Com ```kotlin package com.jetbrains.spacetutorial - - import androidx.compose.material3.ExperimentalMaterial3Api + import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue + import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember + import androidx.compose.runtime.rememberCoroutineScope + import androidx.compose.runtime.setValue + import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.androidx.compose.koinViewModel + import androidx.compose.material3.ExperimentalMaterial3Api @OptIn( - ExperimentalMaterial3Api::class + ExperimentalMaterial3Api::class ) @Composable + @Preview fun App() { val viewModel = koinViewModel() val state by remember { viewModel.state } + val coroutineScope = rememberCoroutineScope() + var isRefreshing by remember { mutableStateOf(false) } val pullToRefreshState = rememberPullToRefreshState() - if (pullToRefreshState.isRefreshing) { - viewModel.loadLaunches() - pullToRefreshState.endRefresh() - } } ``` @@ -806,17 +817,23 @@ Create the main `App()` composable for your application, and call it from a `Com 2. Now add the UI code that will implement the loading screen, the column of launch results, and the pull-to-refresh action: - ```kotlin package com.jetbrains.spacetutorial - - import androidx.compose.foundation.layout.* + + import androidx.compose.foundation.layout.Arrangement + import androidx.compose.foundation.layout.Column + import androidx.compose.foundation.layout.Spacer + import androidx.compose.foundation.layout.fillMaxSize + import androidx.compose.foundation.layout.height + import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items - import androidx.compose.material3.* + import androidx.compose.material3.HorizontalDivider + import androidx.compose.material3.MaterialTheme + import androidx.compose.material3.Scaffold + import androidx.compose.material3.Text + import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox - import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState - import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -825,16 +842,19 @@ Create the main `App()` composable for your application, and call it from a `Com import com.jetbrains.spacetutorial.theme.app_theme_successful import com.jetbrains.spacetutorial.theme.app_theme_unsuccessful import kotlinx.coroutines.launch - import org.koin.androidx.compose.koinViewModel - - @OptIn(ExperimentalMaterial3Api::class) + ... + + @OptIn( + ExperimentalMaterial3Api::class + ) @Composable + @Preview fun App() { - val viewModel = koinViewModel() - val state by remember { viewModel.state } - val coroutineScope = rememberCoroutineScope() - var isRefreshing by remember { mutableStateOf(false) } - val pullToRefreshState = rememberPullToRefreshState() + val viewModel = koinViewModel() + val state by remember { viewModel.state } + val coroutineScope = rememberCoroutineScope() + var isRefreshing by remember { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() AppTheme { Scaffold( @@ -899,16 +919,15 @@ Create the main `App()` composable for your application, and call it from a `Com } } ``` - {initial-collapse-state="collapsed" collapsible="true" collapsed-title="import com.jetbrains.spacetutorial.theme.AppTheme"} + {initial-collapse-state="collapsed" collapsible="true" collapsed-title="import com.jetbrains.spacetutorial.theme.AppTheme"} -3. Finally, specify your `MainActivity` class in the `` tag the `AndroidManifest.xml` file: +3. Finally, specify your `MainActivity` class in the `` tag in the `AndroidManifest.xml` file: ```xml ... - - +