Slide 1

Slide 1 text

Enabling KMP Success: The Android Jetpack Journey Sungyong An KotlinConf’24 South Korea

Slide 2

Slide 2 text

Sungyong An NAVER WEBTOON Android GDE @fornewid

Slide 3

Slide 3 text

য়ט ߊ಴ח. KotlinConf ’24 ࣁ࣌ ղਊਸ ׮ܛפ׮. Link: h tt ps://kotlinconf.com/talks/623652/

Slide 4

Slide 4 text

Kotlin Multiplatform & Jetpack KMP Android iOS Web Windows ... “Libraries and tools to write better Android apps with ease” Link: h tt ps://developer.android.com/jetpack

Slide 5

Slide 5 text

Jetpack KMP ...? ? 🤔

Slide 6

Slide 6 text

What makes Android successful? A thriving ecosystem: • Create apps that delight users • Businesses that are profitable • Professionals with successful careers KMP's impact: • Smooth native interoperability • Reduced time to market • Transferable skills

Slide 7

Slide 7 text

KMPify everything? • Jetpack has ~800 modules 😅 • Not every library needs KMP • Each platform adds significant infra load

Slide 8

Slide 8 text

History of Jetpack KMP 2020 2021 2022 2023 2024 Alpha releases d.android.com Official KMP support 🎉 🎉 Dev Libraries First product review Experiments with Workspace

Slide 9

Slide 9 text

Libraries • Ready today: • Annotations, Collections, DataStore • Commonified: • Lifecycle, ViewModel, Paging • And then, the most requested... Link: h tt ps://developer.android.com/kotlin/multipla tf orm

Slide 10

Slide 10 text

Making “Room” for KMP Success

Slide 11

Slide 11 text

Room is a Jetpack library that makes it easier to use SQLite databases in applications, by using its annotation APIs that generate code. Link: h tt ps://developer.android.com/training/data-storage/room

Slide 12

Slide 12 text

Entity @Entity data class Song ( @PrimaryKey val songId: Long, val title: String ) @Entity data class Album ( @PrimaryKey val albumId: Long ) @Entity data class Artist ( @PrimaryKey val artistId: Long )

Slide 13

Slide 13 text

Data Access Object “DAO” @Dao interface MusicDao { @Insert suspend fun insertSong(song: Song) @Query("SELECT * FROM Song ORDER BY title ASC") fun getAllSongs(): Flow> }

Slide 14

Slide 14 text

Database @Database( entities = [ Song::class, Album::class, Artist::class ], version = 1 ) abstract class MusicDatabase : RoomDatabase() { abstract fun musicDao(): MusicDao } @Entity data class Song( @PrimaryKey val songId: Long, ) @Entity data class Album( @PrimaryKey val albumId: Long ) @Entity data class Artist( @PrimaryKey val artistId: Long )

Slide 15

Slide 15 text

KMP Support Room now supports: • Android • JVM Desktop • Native (iOS, Mac and Linux) Link: h tt ps://developer.android.com/kotlin/multipla tf orm/room

Slide 16

Slide 16 text

Android KMP ? 🤔

Slide 17

Slide 17 text

KMP Android ? 😱

Slide 18

Slide 18 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen

Slide 19

Slide 19 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen

Slide 20

Slide 20 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen

Slide 21

Slide 21 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen

Slide 22

Slide 22 text

Tooling for Success: XProcessing, XPoet & XProcessingTesting

Slide 23

Slide 23 text

XProcessing Abstraction over Java Annotation Processing (JavaAP) and Kotlin Symbol Processing (KSP) Has APIs to represent: • XType <- JavaAP’s TypeMirror or KSP’s KSType • XTypeElement <- JavaAP’s TypeElement or KSP’s KSClassDeclaration • XMethodElement <- JavaAP’s ExecutableElement or KSP’s KSFunctionDeclaration … and more! Link: h tt ps://cs.android.com/androidx/pla tf orm/frameworks/suppo rt /+/.../room-compiler-processing/

Slide 24

Slide 24 text

Database Declaration abstract class MusicDatabase XTypeElement

Slide 25

Slide 25 text

Database Declaration abstract class MusicDatabase : RoomDatabase() { abstract fun musicDao(): MusicDao } XExecutableElement

Slide 26

Slide 26 text

Database Declaration @Database( entities = [ Song::class, Album::class, Artist::class, ], version = 1 ) abstract class MusicDatabase : RoomDatabase() { abstract fun musicDao(): MusicDao } XType

Slide 27

Slide 27 text

Database Declaration @Database( entities = [ Song::class, Album::class, Artist::class, ], version = 1 ) abstract class MusicDatabase : RoomDatabase() { abstract fun musicDao(): MusicDao }

Slide 28

Slide 28 text

DatabaseProcessingStep.kt class DatabaseProcessingStep

Slide 29

Slide 29 text

DatabaseProcessingStep.kt class DatabaseProcessingStep : XProcessingStep { }

Slide 30

Slide 30 text

DatabaseProcessingStep.kt class DatabaseProcessingStep : XProcessingStep { override fun process( ... ): Set { } }

Slide 31

Slide 31 text

DatabaseProcessingStep.kt class DatabaseProcessingStep : XProcessingStep { override fun process( ... ): Set { val databases = elementsByAnnotation[Database::class.qualifiedName] ?.filterIsInstance() ?.mapNotNull { annotatedElement -> DatabaseProcessor(annotatedElement).process() } databases?.forEach { db -> // generate code } } }

Slide 32

Slide 32 text

DatabaseProcessingStep.kt class DatabaseProcessingStep : XProcessingStep { override fun process( ... ): Set { val databases = elementsByAnnotation[Database::class.qualifiedName] ?.filterIsInstance() ?.mapNotNull { annotatedElement -> DatabaseProcessor(annotatedElement).process() } databases?.forEach { db -> // generate code } } }

Slide 33

Slide 33 text

@Entity data class Song( @PrimaryKey val songId: Long ) // Access via property syntax val _songId = item.songId Entity Declaration // Access via property getter val _songId = item.getSongId(); Kotlin output Java output

Slide 34

Slide 34 text

EntityProcessor.kt val getterCandidates = typeElement.getAllMethods()

Slide 35

Slide 35 text

EntityProcessor.kt val getterCandidates = typeElement.getAllMethods().filter { it.element.parameters.isEmpty() && it.resolvedType.returnType.asTypeName() != XTypeName.UNIT_VOID }

Slide 36

Slide 36 text

EntityProcessor.kt val getterCandidates = typeElement.getAllMethods().filter { it.element.parameters.isEmpty() && it.resolvedType.returnType.asTypeName() != XTypeName.UNIT_VOID } // ... getterCandidates.forEach { getter -> if (targetLanguage == CodeLanguage.KOTLIN) }

Slide 37

Slide 37 text

EntityProcessor.kt val getterCandidates = typeElement.getAllMethods().filter { it.element.parameters.isEmpty() && it.resolvedType.returnType.asTypeName() != XTypeName.UNIT_VOID } // ... getterCandidates.forEach { getter -> if (targetLanguage == CodeLanguage.KOTLIN && getter.isKotlinPropertyGetter()) { // generate property accessor call } else { // generate getter call } }

Slide 38

Slide 38 text

XPoet A cross-language API mirrors JavaPoet and KotlinPoet APIs. • XTypeName: Represents a type name in Java and Kotlin's type system. • XFunSpec: Represents a Java/Kotlin function. • XCodeBlock: Represents a snippet of JPL/Kotlin code, supporting placeholders for values, names, types, and members. … and more!

Slide 39

Slide 39 text

DatabaseWriter.kt fun create() : MusicDao { } MusicDao create() { } (Generated Code) Kotlin output Java output

Slide 40

Slide 40 text

DatabaseWriter.kt fun create() : MusicDao { return MusicDao_Impl() } MusicDao create() { return new MusicDao_Impl(); } (Generated Code) Kotlin output Java output

Slide 41

Slide 41 text

DatabaseWriter.kt private fun createFunction( dao: Dao ): FunSpec { return FunSpec.builder("create") .addModifiers(KModifier.PUBLIC) .returns(dao.typeName) .addStatement( "return %T", dao.implTypeName ).build() } private fun createFunction( dao: Dao ): MethodSpec { return MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC) .returns(dao.typeName) .addStatement( "return new $T()", dao.implTypeName ).build() } KotlinPoet JavaPoet

Slide 42

Slide 42 text

DatabaseWriter.kt private fun createFunction( dao: Dao ): FunSpec { return FunSpec.builder("create") .addModifiers(KModifier.PUBLIC) .returns(dao.typeName) .addStatement( "return %T", dao.implTypeName ).build() } private fun createFunction( dao: Dao ): MethodSpec { return MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC) .returns(dao.typeName) .addStatement( "return new $T()", dao.implTypeName ).build() } KotlinPoet JavaPoet

Slide 43

Slide 43 text

DatabaseWriter.kt private fun createFunction( dao: Dao ): FunSpec { return FunSpec.builder("create") .addModifiers(KModifier.PUBLIC) .returns(dao.typeName) .addStatement( "return %T", dao.implTypeName ).build() } private fun createFunction( dao: Dao ): MethodSpec { return MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC) .returns(dao.typeName) .addStatement( "return new $T()", dao.implTypeName ).build() } KotlinPoet JavaPoet

Slide 44

Slide 44 text

DatabaseWriter.kt private fun createFunction( dao: Dao ): FunSpec { return FunSpec.builder("create") .addModifiers(KModifier.PUBLIC) .returns(dao.typeName) .addStatement( "return %T", dao.implTypeName ).build() } private fun createFunction( dao: Dao ): MethodSpec { return MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC) .returns(dao.typeName) .addStatement( "return new $T()", dao.implTypeName ).build() } KotlinPoet JavaPoet

Slide 45

Slide 45 text

DatabaseWriter.kt (XPoet) private fun createFunction( codeLanguage: CodeLanguage, dao: Dao ): XFunSpec { }

Slide 46

Slide 46 text

DatabaseWriter.kt (XPoet) private fun createFunction( codeLanguage: CodeLanguage, dao: Dao ): XFunSpec { return XFunSpec.builder(codeLanguage, "create", VisibilityModifier.PUBLIC).apply { }.build() }

Slide 47

Slide 47 text

DatabaseWriter.kt (XPoet) private fun createFunction( codeLanguage: CodeLanguage, dao: Dao ): XFunSpec { return XFunSpec.builder(codeLanguage, "create", VisibilityModifier.PUBLIC).apply { returns(dao.typeName) addStatement( "return %L", XCodeBlock.ofNewInstance(codeLanguage, dao.implTypeName) ) }.build() }

Slide 48

Slide 48 text

SQLite.kt package androidx.sqlite /** * Executes a single SQL statement that returns no values. */ fun SQLiteConnection.execSQL(sql: String) { prepare(sql).use { it.step() } }

Slide 49

Slide 49 text

AutoMigrationWriter.kt public override fun migrate( ) { } @Override public void migrate( ) { } (Generated Code) Kotlin output Java output

Slide 50

Slide 50 text

AutoMigrationWriter.kt public override fun migrate( connection: SQLiteConnection ) { } @Override public void migrate( @NonNull final SQLiteConnection connection ) { } (Generated Code) Kotlin output Java output

Slide 51

Slide 51 text

AutoMigrationWriter.kt public override fun migrate( connection: SQLiteConnection ) { connection.execSQL("...") } @Override public void migrate( @NonNull final SQLiteConnection connection ) { SQLiteKt.execSQL(connection, "..."); } (Generated Code) Kotlin output Java output

Slide 52

Slide 52 text

AutoMigrationWriter.kt public override fun migrate( connection: SQLiteConnection ) { connection.execSQL("...") } @Override public void migrate( @NonNull final SQLiteConnection connection ) { SQLiteKt.execSQL(connection, "..."); } (Generated Code) Kotlin output Java output

Slide 53

Slide 53 text

AutoMigrationWriter.kt override fun addDatabaseExecuteSqlStatement( migrateBuilder: XFunSpec.Builder, sql: String ) { migrateBuilder.addStatement( "%L", XCodeBlock.ofExtensionCall( language = codeLanguage, memberName = SQLiteDriverMemberNames.CONNECTION_EXEC_SQL, receiverVarName = "connection", args = XCodeBlock.of(codeLanguage, "%S", sql) ) ) }

Slide 54

Slide 54 text

Compromise: “Java-ish Kotlin” public override fun listOfString(arg: List): MyEntity { } (Generated Code)

Slide 55

Slide 55 text

Compromise: “Java-ish Kotlin” public override fun listOfString(arg: List): MyEntity { val _stringBuilder: StringBuilder = StringBuilder() ... } (Generated Code)

Slide 56

Slide 56 text

Compromise: “Java-ish Kotlin” public override fun listOfString(arg: List): MyEntity { val _stringBuilder: StringBuilder = StringBuilder() _stringBuilder.append("SELECT * FROM MyEntity WHERE string IN (") val _inputSize: Int = arg.size appendPlaceholders(_stringBuilder, _inputSize) _stringBuilder.append(")") val _sql: String = _stringBuilder.toString() return performBlocking(__db, true, false) { _connection -> val _stmt: SQLiteStatement = _connection.prepare(_sql) try { var _argIndex: Int = 1 for (_item: String in arg) { _stmt.bindText(_argIndex, _item) _argIndex++ } ... (Generated Code)

Slide 57

Slide 57 text

XProcessing Testing XProcessing Testing is an auxiliary API that simplifies testing across KAPT, KSP, and Javac environments. • Ensured backwards compatibility • Reduce test duplication due to various environments.

Slide 58

Slide 58 text

DatabaseProcessorTest.kt val dbSrc = Source.kotlin( "MyDatabase.kt", """ """.trimIndent() )

Slide 59

Slide 59 text

DatabaseProcessorTest.kt val dbSrc = Source.kotlin( "MyDatabase.kt", """ import androidx.room.* @Database(entities = [MyEntity::class], version = 1) abstract class MyDatabase """.trimIndent() ) : RoomDatabase

Slide 60

Slide 60 text

DatabaseProcessorTest.kt val dbSrc = Source.kotlin( ... ) val entitySrc = Source.java( "MyEntity", """ import androidx.room.* class MyEntity { @PrimaryKey long id; } """.trimIndent() )

Slide 61

Slide 61 text

DatabaseProcessorTest.kt @Test fun validateSuperclass() { runProcessorTest( sources = listOf(dbSrc, entitySrc), createProcessingSteps = { listOf(DatabaseProcessingStep()) } ) { } } Link: h tt ps://cs.android.com/.../room-compiler-processing-testing/src/.../ProcessorTestExt.kt

Slide 62

Slide 62 text

DatabaseProcessorTest.kt @Test fun validateSuperclass() { runProcessorTest( sources = listOf(dbSrc, entitySrc), createProcessingSteps = { listOf(DatabaseProcessingStep()) } ) { result -> result.hasErrorContaining( "Classes annotated with @Database should extend androidx.room.RoomDatabase" ) } } Link: h tt ps://cs.android.com/.../room-compiler-processing-testing/src/.../ProcessorTestExt.kt

Slide 63

Slide 63 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen Conversion to Kotlin-Multiplatform

Slide 64

Slide 64 text

The Conversion To KMP

Slide 65

Slide 65 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen

Slide 66

Slide 66 text

KMP SQLite & Build Process • Kotlin/Native’s Clang and sysroots to cross-compile SQLite • C-interop for SQLite access within Kotlin code • Development of bundled and framework SQLite drivers Link: h tt ps://developer.android.com/kotlin/multipla tf orm/sqlite

Slide 67

Slide 67 text

Framework SQLite Android Common Native Android Native sqlite3.h android.database. SQLiteDatabase Android SQLite Driver Native SQLite Driver room-runtime sqlite-framework

Slide 68

Slide 68 text

Common Bundled SQLite Android Common Native Android Native sqlite3.c sqlite_jni.cpp room-runtime sqlite-bundled Bundled SQLite Driver

Slide 69

Slide 69 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen

Slide 70

Slide 70 text

Room’s Migration Journey Room KMP! Kotlin Symbol Processing (KSP) Support Migrate Runtime JPL -> Kotlin Support Kotlin Codegen Add cross-platform SQLite support Migrate to SQLite drivers Cross-platform codegen

Slide 71

Slide 71 text

Maintaining Compatibility • Surveyed Room APIs • Move compatible APIs to common • Retain Android-only functionality

Slide 72

Slide 72 text

Expect / Actuals // src/commonMain/.../FileLock.kt internal expect class FileLock(filename: String) { fun lock() fun unlock() } Strategy

Slide 73

Slide 73 text

Expect / Actuals // src/nativeMain/.../FileLock.native.kt import platform.posix.* internal actual class FileLock actual constructor(filename: String) { Strategy

Slide 74

Slide 74 text

Expect / Actuals // src/nativeMain/.../FileLock.native.kt import platform.posix.* internal actual class FileLock actual constructor(filename: String) { // src/androidJvmMain/.../FileLock.androidJvm.kt import java.io.* import java.nio.channels.* internal actual class FileLock actual constructor(filename: String) { Strategy

Slide 75

Slide 75 text

Interfaces and Implementations // src/commonMain/.../SQLiteDriver.kt interface SQLiteDriver { fun open(fileName: String): SQLiteConnection } Strategy

Slide 76

Slide 76 text

Interfaces and Implementations // src/nativeMain/.../NativeSQLiteDriver.native.kt class NativeSQLiteDriver : SQLiteDriver { override fun open(fileName: String): SQLiteConnection = memScoped { val dbPointer = allocPointerTo() val resultCode = sqlite3_open_v2( filename = fileName, ppDb = dbPointer.ptr, flags = SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE, zVfs = null ) if (resultCode != SQLITE_OK) { throwSQLiteException(resultCode, null) } NativeSQLiteConnection(dbPointer.value!!) } } Strategy

Slide 77

Slide 77 text

Interfaces and Implementations // src/androidJvmMain/.../BundledSQLiteDriver.androidJvm.kt actual class BundledSQLiteDriver : SQLiteDriver { override fun open(fileName: String): SQLiteConnection { val address = nativeOpen(fileName) return BundledSQLiteConnection(address) } private companion object { init { NativeLibraryLoader.loadLibrary("sqliteJni") } } } private external fun nativeOpen(name: String): Long Strategy

Slide 78

Slide 78 text

Top-Level Declarations // src/commonMain/.../TableInfo.kt expect class TableInfo { override fun equals(other: Any?): Boolean override fun hashCode(): Int } Strategy

Slide 79

Slide 79 text

Top-Level Declarations // src/commonMain/.../TableInfo.kt expect class TableInfo { override fun equals(other: Any?): Boolean override fun hashCode(): Int } internal fun TableInfo.equalsCommon(other: Any?): Boolean { // ... } internal fun TableInfo.hashCodeCommon(): Int { // ... } Strategy

Slide 80

Slide 80 text

Top-Level Declarations // src/jvmNativeMain/.../TableInfo.jvmNative.kt actual class TableInfo { actual override fun equals(other: Any?) = equalsCommon(other) actual override fun hashCode() = hashCodeCommon() } // src/androidMain/.../TableInfo.android.kt actual class TableInfo { actual override fun equals(other: Any?) = equalsCommon(other) actual override fun hashCode() = hashCodeCommon() companion object { fun read(database: SupportSQLiteDatabase) { ... } } } Strategy

Slide 81

Slide 81 text

Additional Problems • Heavy reliance on Executors, Runnables and Callables. • The use of reflection while accessing the generated database implementation.

Slide 82

Slide 82 text

Reflection KMP fun createRoomDatabase() = Room.inMemoryDatabaseBuilder( context = appContext ).build() @Generated(value = ["androidx.room.RoomProcessor"]) public class MusicDatabase_Impl : MusicDatabase() { // ... } Class.forName(databaseClass + "_Impl")

Slide 83

Slide 83 text

Reflection KMP fun createRoomDatabase() = Room.inMemoryDatabaseBuilder( context = appContext, factory = { MusicDatabase_Impl() } ).build() Link: h tt ps://youtrack.jetbrains.com/issue/KT-61724/Re fl ection-suppo rt -for-kmp @Generated(value = ["androidx.room.RoomProcessor"]) public class MusicDatabase_Impl : MusicDatabase() { // ... }

Slide 84

Slide 84 text

Room’s Migration Journey Room KMP! Build XProcessing Add KSP support in room Move JPL sources to be Kotlin sources Add cross-platform SQLite support Migrate sources to use cross-platform SQLite drivers Generate cross-platform compatible code Add Kotlin code generation

Slide 85

Slide 85 text

Kotlin Multiplatform’s Bright Future • Broadening Non-Android Support The alpha release marks a significant milestone in Room’s KMP journey, with non-Android platforms currently nearing full feature parity. • Expanded Platform Support Future development plans for Room include expansion to Web via SQLite WASM. • Google's Official Investment in KMP Room's adoption of KMP aligns with Google’s continued investment in KMP, contributing to the growth and success of the ecosystem.

Slide 86

Slide 86 text

Libraries: Android vs KMP Language UI Image Loading DI Java, Kotlin Kotlin View, Jetpack Compose Compose Multiplatform Glide, Coil Coil Network Database Storage Preference, DataStore DataStore Retrofit Ktor Dagger, Hilt Koin, Kodein, kotlin-inject Room SQLDelight Android Kotlin Multiplatform ... ... ...

Slide 87

Slide 87 text

Libraries: Android vs KMP Language UI Image Loading DI Java, Kotlin Kotlin View, Jetpack Compose Compose Multiplatform Glide, Coil Coil Network Database Storage Preference, DataStore DataStore Retrofit Ktor Dagger, Hilt Koin, Kodein, kotlin-inject Room 🆕 Room, SQLDelight Android Kotlin Multiplatform ... ... ... 🎉🎉

Slide 88

Slide 88 text

੿ܻ • Room੉ KMPܳ ૑ਗೞחؘ ٜযр ֢۱: • KSP ૑ਗ (XProcessing, XPoet) • ࣗझ௏٘ ߂ ࢤࢿغח ௏٘ܳ Kotlinਵ۽ ੹ജ • SQLite ۄ੉࠳۞ܻ ௼۽झ೒ۖಬ ૑ਗ • Android জ ѐߊ੗ ੑ੢ীࢲ: • Breaking Changesח হ׮. • Data Migration হ੉, জਸ KMP۽ ഛ੢ೡ ࣻب ੓׮. • ׮݅ KMP۽ ഛ੢ೞ۰ݶ Room Migration੉ ೙ਃೞ׮. (SQLite Driver ١) Link: h tt ps://developer.android.com/training/data-storage/room/room-kmp-migration

Slide 89

Slide 89 text

хࢎ೤פ׮!