Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Goes Global - Android Worldwide

Kotlin Goes Global - Android Worldwide

Karan Dhillon

April 27, 2021
Tweet

More Decks by Karan Dhillon

Other Decks in Technology

Transcript

  1. Trip down the memory lane • There has been numerous

    attempts on creating the best hybrid mobile framework in the past. • Most notable candidates include React Native, Flutter, Xamarin, and Ionic. • In order to understand why KMM is a gamechanger, we first have to briefly look into these past solutions.
  2. • Biggest selling point is one codebase. This could also

    be extended to time saved on writing or managing one codebase. • Fast - in shipping, in deploying, and in developing. • Team size can be kept small, in return saving business expense. • Slave to the framework. • Starts fragmenting when TLOC gets bigger. The Good, The Bad, And The Ugly
  3. Why is JetBrains jumping into the world of hybrid frameworks?

    The Question Remains Kotlin, a JVM based language, is very rapidly being adopted by the android community. Based on the demographic android covers, Kotlin would be an easy candidate for a hybrid framework.
  4. “Kotlin Multiplatform Mobile (KMM) is an SDK for cross-platform mobile

    development provided by JetBrains. It uses the multiplatform capabilities of Kotlin and includes various tools and features designed to make the end-to-end experience of building mobile cross-platform applications as enjoyable and efficient as possible.”
  5. //Common expect fun randomUUID(): String //Android import java.util.* actual fun

    randomUUID() = UUID.randomUUID().toString() //iOS import platform.Foundation.NSUUID actual fun randomUUID(): String = NSUUID().UUIDString()
  6. //iOS import platform.Foundation.NSUUID actual fun randomUUID(): String = NSUUID().UUIDString() //Android

    import java.util.* actual fun randomUUID() = UUID.randomUUID().toString() //Common expect fun randomUUID(): String
  7. commonMain directory will hold the business logic that will be

    shared among all the platforms shared module will hold the shared business logic. This logic can be further dissected into platform specific directories androidMain & iosMain directories will hold the business logic that will be shared among the respective platform’s feature module
  8. iOSTest directory will hold the business logic that can be

    reached by iOS module Compose-desktop module will hold the implementation logic for desktop support
  9. KMM project requirements • Android studio, version 4.2 or higher

    • If iOS specific-code needs to be written, then Xcode version 11.3 or higher • Compatible Kotlin compiler plugin • Kotlin Multiplatform Mobile plugin in Android Studio • JDK
  10. commonMain directory will hold the business logic that will be

    shared among all the platforms shared module will hold the shared business logic. This logic can be further dissected into platform specific directories androidMain & iosMain directories will hold the business logic that will be shared among the respective platform’s feature module
  11. plugins { kotlin("multiplatform") id("com.android.library") } kotlin { val coroutinesVersion =

    "1.3.9-native-mt" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") } } } } Build.gradle.kts (shared)
  12. plugins { kotlin("multiplatform") id("com.android.library") } kotlin { val coroutinesVersion =

    "1.3.9-native-mt" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") } } } } Build.gradle.kts (shared)
  13. buildscript { repositories { gradlePluginPortal() jcenter() google() mavenCentral() } dependencies

    { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10") classpath("com.android.tools.build:gradle:4.0.1") classpath("org.jetbrains.kotlin:kotlin-serialization:1.4.10") } } allprojects { repositories { google() jcenter() mavenCentral() } } Build.gradle.kts (root)
  14. buildscript { repositories { gradlePluginPortal() jcenter() google() mavenCentral() } dependencies

    { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10") classpath("com.android.tools.build:gradle:4.0.1") classpath("org.jetbrains.kotlin:kotlin-serialization:1.4.10") } } allprojects { repositories { google() jcenter() mavenCentral() } } Build.gradle.kts (root)
  15. plugins { kotlin("multiplatform") id("com.android.library") kotlin("plugin.serialization") } kotlin { val coroutinesVersion

    = "1.3.9-native-mt" val serializationVersion = "1.0.0-RC" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") } } } } Build.gradle.kts (shared)
  16. plugins { kotlin("multiplatform") id("com.android.library") kotlin("plugin.serialization") } kotlin { val coroutinesVersion

    = "1.3.9-native-mt" val serializationVersion = "1.0.0-RC" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") } } } } Build.gradle.kts (shared)
  17. Build.gradle.kts (shared) kotlin { val coroutinesVersion = "1.3.9-native-mt" val serializationVersion

    = "1.0.0-RC" val ktorVersion = "1.4.0" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-serialization:$ktorVersion") } } val androidMain by getting { dependencies { implementation("io.ktor:ktor-client-android:$ktorVersion") } } val iosMain by getting { dependencies { implementation("io.ktor:ktor-client-ios:$ktorVersion") } } } }
  18. kotlin { val coroutinesVersion = "1.3.9-native-mt" val serializationVersion = "1.0.0-RC"

    val ktorVersion = "1.4.0" sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-serialization:$ktorVersion") } } val androidMain by getting { dependencies { implementation("io.ktor:ktor-client-android:$ktorVersion") } } val iosMain by getting { dependencies { implementation("io.ktor:ktor-client-ios:$ktorVersion") } } } } Build.gradle.kts (shared)
  19. buildscript { repositories { gradlePluginPortal() jcenter() google() mavenCentral() } val

    sqlDelightVersion: String by project dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10") classpath("com.android.tools.build:gradle:4.0.1") classpath("org.jetbrains.kotlin:kotlin-serialization:1.4.10") classpath("com.squareup.sqldelight:gradle-plugin:$sqlDelightVersion") } } allprojects { repositories { google() jcenter() mavenCentral() } } Build.gradle.kts (root)
  20. buildscript { repositories { gradlePluginPortal() jcenter() google() mavenCentral() } val

    sqlDelightVersion: String by project dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10") classpath("com.android.tools.build:gradle:4.0.1") classpath("org.jetbrains.kotlin:kotlin-serialization:1.4.10") classpath("com.squareup.sqldelight:gradle-plugin:$sqlDelightVersion") } } allprojects { repositories { google() jcenter() mavenCentral() } } Build.gradle.kts (root)
  21. Build.gradle.kts (shared) plugins { id("com.squareup.sqldelight") } kotlin { val sqlDelightVersion:

    String by project sourceSets { val commonMain by getting { dependencies { implementation("com.squareup.sqldelight:runtime:$sqlDelightVersion") } } val androidMain by getting { dependencies { implementation("com.squareup.sqldelight:android-driver:$sqlDelightVersion") } } val iosMain by getting { dependencies { implementation("com.squareup.sqldelight:native-driver:$sqlDelightVersion") } } } }
  22. plugins { id("com.squareup.sqldelight") } kotlin { val sqlDelightVersion: String by

    project sourceSets { val commonMain by getting { dependencies { implementation("com.squareup.sqldelight:runtime:$sqlDelightVersion") } } val androidMain by getting { dependencies { implementation("com.squareup.sqldelight:android-driver:$sqlDelightVersion") } } val iosMain by getting { dependencies { implementation("com.squareup.sqldelight:native-driver:$sqlDelightVersion") } } } } Build.gradle.kts (shared)
  23. Entity.kt(com.kdhillon.spacex.shared.entity) @Serializable data class RocketLaunch( @SerialName("flight_number") val flightNumber: Int, @SerialName("mission_name")

    val missionName: String, @SerialName("launch_year") val launchYear: Int, @SerialName("launch_date_utc") val launchDateUTC: String, @SerialName("rocket") val rocket: Rocket, @SerialName("details") val details: String?, @SerialName("launch_success") val launchSuccess: Boolean?, @SerialName("links") val links: Links ) @Serializable data class Rocket( @SerialName("rocket_id") val id: String, @SerialName("rocket_name") val name: String, @SerialName("rocket_type") val type: String ) @Serializable data class Links( @SerialName("mission_patch") val missionPatchUrl: String?, @SerialName("article_link") val articleUrl: String? )
  24. @Serializable data class RocketLaunch( @SerialName("flight_number") val flightNumber: Int, @SerialName("mission_name") val

    missionName: String, @SerialName("launch_year") val launchYear: Int, @SerialName("launch_date_utc") val launchDateUTC: String, @SerialName("rocket") val rocket: Rocket, @SerialName("details") val details: String?, @SerialName("launch_success") val launchSuccess: Boolean?, @SerialName("links") val links: Links ) @Serializable data class Rocket( @SerialName("rocket_id") val id: String, @SerialName("rocket_name") val name: String, @SerialName("rocket_type") val type: String ) @Serializable data class Links( @SerialName("mission_patch") val missionPatchUrl: String?, @SerialName("article_link") val articleUrl: String? ) Entity.kt(com.kdhillon.spacex.shared.entity)
  25. Build.gradle.kts (shared) plugins { // ... } kotlin { //

    ... } sqldelight { database("AppDatabase") { // The packageName parameter specifies the package name for the generated Kotlin sources. packageName = "com.kdhillon.spacex.shared.cache" } }
  26. plugins { // ... } kotlin { // ... }

    sqldelight { database("AppDatabase") { // The packageName parameter specifies the package name for the generated Kotlin sources. packageName = "com.kdhillon.spacex.shared.cache" } } Build.gradle.kts (shared)
  27. First we need to create .sq file, which will contain

    all the SQL queries. By default, SQLDelight plugin reads .sq from the sqldelight folder. So create the package specified in the packageName parameter.
  28. Inside our package, we need to create a .sq file

    with the name of our database (in our case, AppDatabase.sq)
  29. AppDatabase.sq CREATE TABLE Launch ( flightNumber INTEGER NOT NULL, missionName

    TEXT NOT NULL, launchYear INTEGER AS Int NOT NULL DEFAULT 0, rocketId TEXT NOT NULL, details TEXT, launchSuccess INTEGER AS Boolean DEFAULT NULL, launchDateUTC TEXT NOT NULL, missionPatchUrl TEXT, articleUrl TEXT ); CREATE TABLE Rocket ( id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL );
  30. CREATE TABLE Launch ( flightNumber INTEGER NOT NULL, missionName TEXT

    NOT NULL, launchYear INTEGER AS Int NOT NULL DEFAULT 0, rocketId TEXT NOT NULL, details TEXT, launchSuccess INTEGER AS Boolean DEFAULT NULL, launchDateUTC TEXT NOT NULL, missionPatchUrl TEXT, articleUrl TEXT ); CREATE TABLE Rocket ( id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL ); AppDatabase.sq
  31. AppDatabase.sq CREATE TABLE Launch ( flightNumber INTEGER NOT NULL, missionName

    TEXT NOT NULL, launchYear INTEGER AS Int NOT NULL DEFAULT 0, rocketId TEXT NOT NULL, details TEXT, launchSuccess INTEGER AS Boolean DEFAULT NULL, launchDateUTC TEXT NOT NULL, missionPatchUrl TEXT, articleUrl TEXT ); CREATE TABLE Rocket ( id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL );
  32. insertLaunch: INSERT INTO Launch(flightNumber, missionName, launchYear, rocketId, details, launchSuccess, launchDateUTC,

    missionPatchUrl, articleUrl) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?); insertRocket: INSERT INTO Rocket(id, name, type) VALUES(?, ?, ?); AppDatabase.sq
  33. insertLaunch: INSERT INTO Launch(flightNumber, missionName, launchYear, rocketId, details, launchSuccess, launchDateUTC,

    missionPatchUrl, articleUrl) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?); insertRocket: INSERT INTO Rocket(id, name, type) VALUES(?, ?, ?); AppDatabase.sq
  34. insertLaunch: INSERT INTO Launch(flightNumber, missionName, launchYear, rocketId, details, launchSuccess, launchDateUTC,

    missionPatchUrl, articleUrl) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?); insertRocket: INSERT INTO Rocket(id, name, type) VALUES(?, ?, ?); AppDatabase.sq
  35. selectRocketById: SELECT * FROM Rocket WHERE id = ?; selectAllLaunchesInfo:

    SELECT Launch.*, Rocket.* FROM Launch LEFT JOIN Rocket ON Rocket.id == Launch.rocketId; AppDatabase.sq
  36. selectRocketById: SELECT * FROM Rocket WHERE id = ?; selectAllLaunchesInfo:

    SELECT Launch.*, Rocket.* FROM Launch LEFT JOIN Rocket ON Rocket.id == Launch.rocketId; AppDatabase.sq
  37. selectRocketById: SELECT * FROM Rocket WHERE id = ?; selectAllLaunchesInfo:

    SELECT Launch.*, Rocket.* FROM Launch LEFT JOIN Rocket ON Rocket.id == Launch.rocketId; AppDatabase.sq
  38. When the project is compiled, the generated kotlin code will

    be stored in our packageName directory.
  39. interface AppDatabase : Transacter { val appDatabaseQueries: AppDatabaseQueries companion object

    { val Schema: SqlDriver.Schema get() = AppDatabase::class.schema operator fun invoke(driver: SqlDriver): AppDatabase = AppDatabase::class.newInstance(driver)} } AppDatabase.kt(com.kdhillon.spacex.shared.cache)
  40. interface AppDatabase : Transacter { val appDatabaseQueries: AppDatabaseQueries companion object

    { val Schema: SqlDriver.Schema get() = AppDatabase::class.schema operator fun invoke(driver: SqlDriver): AppDatabase = AppDatabase::class.newInstance(driver)} } AppDatabase.kt(com.kdhillon.spacex.shared.cache)
  41. interface AppDatabase : Transacter { val appDatabaseQueries: AppDatabaseQueries companion object

    { val Schema: SqlDriver.Schema get() = AppDatabase::class.schema operator fun invoke(driver: SqlDriver): AppDatabase = AppDatabase::class.newInstance(driver)} } AppDatabase.kt(com.kdhillon.spacex.shared.cache)
  42. interface AppDatabaseQueries : Transacter { fun <T : Any> selectRocketById(id:

    String, mapper: ( id: String, name: String, type: String ) -> T): Query<T> fun selectRocketById(id: String): Query<Rocket> fun <T : Any> selectAllLaunchesInfo(mapper: ( flightNumber: Long, missionName: String, launchYear: Int, rocketId: String, details: String?, launchSuccess: Boolean?, launchDateUTC: String, missionPatchUrl: String?, articleUrl: String?, id: String?, name: String?, type: String? ) -> T): Query<T> fun selectAllLaunchesInfo(): Query<SelectAllLaunchesInfo> ... AppDatabaseQueries.kt(com.kdhillon.spacex.shared.cache)
  43. interface AppDatabaseQueries : Transacter { fun <T : Any> selectRocketById(id:

    String, mapper: ( id: String, name: String, type: String ) -> T): Query<T> fun selectRocketById(id: String): Query<Rocket> fun <T : Any> selectAllLaunchesInfo(mapper: ( flightNumber: Long, missionName: String, launchYear: Int, rocketId: String, details: String?, launchSuccess: Boolean?, launchDateUTC: String, missionPatchUrl: String?, articleUrl: String?, id: String?, name: String?, type: String? ) -> T): Query<T> fun selectAllLaunchesInfo(): Query<SelectAllLaunchesInfo> ... AppDatabaseQueries.kt(com.kdhillon.spacex.shared.cache)
  44. interface AppDatabaseQueries : Transacter { fun <T : Any> selectRocketById(id:

    String, mapper: ( id: String, name: String, type: String ) -> T): Query<T> fun selectRocketById(id: String): Query<Rocket> fun <T : Any> selectAllLaunchesInfo(mapper: ( flightNumber: Long, missionName: String, launchYear: Int, rocketId: String, details: String?, launchSuccess: Boolean?, launchDateUTC: String, missionPatchUrl: String?, articleUrl: String?, id: String?, name: String?, type: String? ) -> T): Query<T> fun selectAllLaunchesInfo(): Query<SelectAllLaunchesInfo> ... AppDatabaseQueries.kt(com.kdhillon.spacex.shared.cache)
  45. interface AppDatabase : Transacter { val appDatabaseQueries: AppDatabaseQueries companion object

    { val Schema: SqlDriver.Schema get() = AppDatabase::class.schema operator fun invoke(driver: SqlDriver): AppDatabase = AppDatabase::class.newInstance(driver)} } AppDatabase.kt(com.kdhillon.spacex.shared.cache)
  46. interface AppDatabase : Transacter { val appDatabaseQueries: AppDatabaseQueries companion object

    { val Schema: SqlDriver.Schema get() = AppDatabase::class.schema operator fun invoke(driver: SqlDriver): AppDatabase = AppDatabase::class.newInstance(driver)} } AppDatabase.kt(com.kdhillon.spacex.shared.cache)
  47. actual class DatabaseDriverFactory(private val context: Context) { actual fun createDriver():

    SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db") } } DatabaseDriverFactory.kt(com.kdhillon.spacex.shared.cache in androidMain)
  48. actual class DatabaseDriverFactory(private val context: Context) { actual fun createDriver():

    SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db") } } DatabaseDriverFactory.kt(com.kdhillon.spacex.shared.cache in androidMain)
  49. actual class DatabaseDriverFactory { actual fun createDriver(): SqlDriver { return

    NativeSqliteDriver(AppDatabase.Schema, "test.db") } } DatabaseDriverFactory.kt(com.kdhillon.spacex.shared.cache in iosMain)
  50. actual class DatabaseDriverFactory { actual fun createDriver(): SqlDriver { return

    NativeSqliteDriver(AppDatabase.Schema, "test.db") } } DatabaseDriverFactory.kt(com.kdhillon.spacex.shared.cache in iosMain)
  51. actual class DatabaseDriverFactory { actual fun createDriver(): SqlDriver { return

    NativeSqliteDriver(AppDatabase.Schema, "test.db") } } DatabaseDriverFactory.kt(com.kdhillon.spacex.shared.cache in iosMain)
  52. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache)
  53. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache)
  54. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache) expect class DatabaseDriverFactory { fun createDriver(): SqlDriver }
  55. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache)
  56. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache)
  57. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache) actual class DatabaseDriverFactory(private val context: Context) { actual fun createDriver(): SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db") } }
  58. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache) actual class DatabaseDriverFactory(private val context: Context) { actual fun createDriver(): SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db") } }
  59. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache) actual class DatabaseDriverFactory(private val context: Context) { actual fun createDriver(): SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db") } } actual class DatabaseDriverFactory { actual fun createDriver(): SqlDriver { return NativeSqliteDriver(AppDatabase.Schema, "test.db") } }
  60. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache) actual class DatabaseDriverFactory(private val context: Context) { actual fun createDriver(): SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db") } } actual class DatabaseDriverFactory { actual fun createDriver(): SqlDriver { return NativeSqliteDriver(AppDatabase.Schema, "test.db") } }
  61. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache) actual class DatabaseDriverFactory(private val context: Context) { actual fun createDriver(): SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "test.db") } } actual class DatabaseDriverFactory { actual fun createDriver(): SqlDriver { return NativeSqliteDriver(AppDatabase.Schema, "test.db") } }
  62. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache)
  63. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache)
  64. internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver())

    private val dbQuery = database.appDatabaseQueries } Database.kt(com.kdhillon.spacex.shared.cache)
  65. class SpaceXApi { companion object { private const val LAUNCHES_ENDPOINT

    = "https://api.spacexdata.com/v3/launches" } private val httpClient = HttpClient { install(JsonFeature) { val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } serializer = KotlinxSerializer(json) } } suspend fun getAllLaunches(): List<RocketLaunch> { return httpClient.get(LAUNCHES_ENDPOINT) } } SpaceXApi.kt(com.kdhillon.spacex.shared.network)
  66. class SpaceXApi { companion object { private const val LAUNCHES_ENDPOINT

    = "https://api.spacexdata.com/v3/launches" } private val httpClient = HttpClient { install(JsonFeature) { val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } serializer = KotlinxSerializer(json) } } suspend fun getAllLaunches(): List<RocketLaunch> { return httpClient.get(LAUNCHES_ENDPOINT) } } SpaceXApi.kt(com.kdhillon.spacex.shared.network)
  67. class SpaceXApi { companion object { private const val LAUNCHES_ENDPOINT

    = "https://api.spacexdata.com/v3/launches" } private val httpClient = HttpClient { install(JsonFeature) { val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } serializer = KotlinxSerializer(json) } } suspend fun getAllLaunches(): List<RocketLaunch> { return httpClient.get(LAUNCHES_ENDPOINT) } } SpaceXApi.kt(com.kdhillon.spacex.shared.network)
  68. <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.kdhillon.spacex.androidApp"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="false"

    android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> AndroidManifest.xml(/Users/karandhillon/Desktop/Personal/SpaceX/androidApp/src/main/AndroidManifest.xml)
  69. <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.kdhillon.spacex.androidApp"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="false"

    android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> AndroidManifest.xml(/Users/karandhillon/Desktop/Personal/SpaceX/androidApp/src/main/AndroidManifest.xml)
  70. <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.kdhillon.spacex.androidApp"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="false"

    android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> AndroidManifest.xml(/Users/karandhillon/Desktop/Personal/SpaceX/androidApp/src/main/AndroidManifest.xml)
  71. class SpaceXSDK(databaseDriverFactory: DatabaseDriverFactory) { private val database = Database(databaseDriverFactory) private

    val api = SpaceXApi() @Throws(Exception::class) suspend fun getLaunches(forceReload: Boolean): List<RocketLaunch> { val cachedLaunches = database.getAllLaunches() return if (cachedLaunches.isNotEmpty() && !forceReload) { cachedLaunches } else { api.getAllLaunches().also { database.clearDatabase() database.createLaunches(it) } } } } SpaceXSDK.kt(com.kdhillon.spacex.shared)
  72. plugins { id("com.android.application") kotlin("android") } dependencies { implementation(project(":shared")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") implementation("androidx.core:core-ktx:1.3.1")

    implementation("com.google.android.material:material:1.2.0") implementation("androidx.appcompat:appcompat:1.2.0") implementation("androidx.constraintlayout:constraintlayout:2.0.0") implementation("androidx.recyclerview:recyclerview:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.cardview:cardview:1.0.0") } Build.gradle.kts (androidApp)
  73. plugins { id("com.android.application") kotlin("android") } dependencies { implementation(project(":shared")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") implementation("androidx.core:core-ktx:1.3.1")

    implementation("com.google.android.material:material:1.2.0") implementation("androidx.appcompat:appcompat:1.2.0") implementation("androidx.constraintlayout:constraintlayout:2.0.0") implementation("androidx.recyclerview:recyclerview:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.cardview:cardview:1.0.0") } Build.gradle.kts (androidApp)
  74. plugins { id("com.android.application") kotlin("android") } dependencies { implementation(project(":shared")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") implementation("androidx.core:core-ktx:1.3.1")

    implementation("com.google.android.material:material:1.2.0") implementation("androidx.appcompat:appcompat:1.2.0") implementation("androidx.constraintlayout:constraintlayout:2.0.0") implementation("androidx.recyclerview:recyclerview:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.cardview:cardview:1.0.0") } Build.gradle.kts (androidApp)
  75. class MainActivity : AppCompatActivity() { private val mainScope = MainScope()

    private val sdk = SpaceXSDK(DatabaseDriverFactory(this)) private lateinit var launchesRecyclerView: RecyclerView private lateinit var progressBarView: FrameLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout private val launchesRvAdapter = LaunchesRvAdapter(listOf()) override fun onCreate(savedInstanceState: Bundle?) { // ... } private fun displayLaunches(needReload: Boolean) { progressBarView.isVisible = true mainScope.launch { kotlin.runCatching { sdk.getLaunches(needReload) }.onSuccess { launchesRvAdapter.launches = it launchesRvAdapter.notifyDataSetChanged() }.onFailure { Toast.makeText(this@MainActivity, it.localizedMessage, Toast.LENGTH_SHORT).show() } progressBarView.isVisible = false } } } MainActivity.kt (androidApp)
  76. class MainActivity : AppCompatActivity() { private val mainScope = MainScope()

    private val sdk = SpaceXSDK(DatabaseDriverFactory(this)) private lateinit var launchesRecyclerView: RecyclerView private lateinit var progressBarView: FrameLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout private val launchesRvAdapter = LaunchesRvAdapter(listOf()) override fun onCreate(savedInstanceState: Bundle?) { // ... } private fun displayLaunches(needReload: Boolean) { progressBarView.isVisible = true mainScope.launch { kotlin.runCatching { sdk.getLaunches(needReload) }.onSuccess { launchesRvAdapter.launches = it launchesRvAdapter.notifyDataSetChanged() }.onFailure { Toast.makeText(this@MainActivity, it.localizedMessage, Toast.LENGTH_SHORT).show() } progressBarView.isVisible = false } } } MainActivity.kt (androidApp)
  77. class MainActivity : AppCompatActivity() { private val mainScope = MainScope()

    private val sdk = SpaceXSDK(DatabaseDriverFactory(this)) private lateinit var launchesRecyclerView: RecyclerView private lateinit var progressBarView: FrameLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout private val launchesRvAdapter = LaunchesRvAdapter(listOf()) override fun onCreate(savedInstanceState: Bundle?) { // ... } private fun displayLaunches(needReload: Boolean) { progressBarView.isVisible = true mainScope.launch { kotlin.runCatching { sdk.getLaunches(needReload) }.onSuccess { launchesRvAdapter.launches = it launchesRvAdapter.notifyDataSetChanged() }.onFailure { Toast.makeText(this@MainActivity, it.localizedMessage, Toast.LENGTH_SHORT).show() } progressBarView.isVisible = false } } } MainActivity.kt (androidApp)
  78. class MainActivity : AppCompatActivity() { private val mainScope = MainScope()

    private val sdk = SpaceXSDK(DatabaseDriverFactory(this)) private lateinit var launchesRecyclerView: RecyclerView private lateinit var progressBarView: FrameLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout private val launchesRvAdapter = LaunchesRvAdapter(listOf()) override fun onCreate(savedInstanceState: Bundle?) { // ... } private fun displayLaunches(needReload: Boolean) { progressBarView.isVisible = true mainScope.launch { kotlin.runCatching { sdk.getLaunches(needReload) }.onSuccess { launchesRvAdapter.launches = it launchesRvAdapter.notifyDataSetChanged() }.onFailure { Toast.makeText(this@MainActivity, it.localizedMessage, Toast.LENGTH_SHORT).show() } progressBarView.isVisible = false } } } MainActivity.kt (androidApp)
  79. class MainActivity : AppCompatActivity() { private val mainScope = MainScope()

    private val sdk = SpaceXSDK(DatabaseDriverFactory(this)) private lateinit var launchesRecyclerView: RecyclerView private lateinit var progressBarView: FrameLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout private val launchesRvAdapter = LaunchesRvAdapter(listOf()) override fun onCreate(savedInstanceState: Bundle?) { // ... } private fun displayLaunches(needReload: Boolean) { progressBarView.isVisible = true mainScope.launch { kotlin.runCatching { sdk.getLaunches(needReload) }.onSuccess { launchesRvAdapter.launches = it launchesRvAdapter.notifyDataSetChanged() }.onFailure { Toast.makeText(this@MainActivity, it.localizedMessage, Toast.LENGTH_SHORT).show() } progressBarView.isVisible = false } } } MainActivity.kt (androidApp)
  80. Challenges Ahead With KMM • KMM is still in alpha,

    and the public API’s are expected to be changed frequently given its scale. • Deploying a KMM project in production therefore should be prohibited. • A lot of edge-cases might be encountered when working in KMM, such as how to manage sensitive information. • iOS counterpart receive the shared module as a framework dependency, where the class files are in Objective-C format. Even though using appCode IDE helps a bit in navigating these Obj-C files, having them in Swift would be the ideal case.