Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Android Jetpack supports KMP

Android Jetpack supports KMP

2024년 06월 29일 (토) KotlinConf '24 Global in South Korea 발표자료입니다.
https://festa.io/events/5375

Sungyong An

June 29, 2024
Tweet

More Decks by Sungyong An

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. KMPify everything? • Jetpack has ~800 modules 😅 • Not

    every library needs KMP • Each platform adds significant infra load
  4. 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
  5. 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
  6. 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
  7. 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 )
  8. Data Access Object “DAO” @Dao interface MusicDao { @Insert suspend

    fun insertSong(song: Song) @Query("SELECT * FROM Song ORDER BY title ASC") fun getAllSongs(): Flow<List<Song>> }
  9. 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 )
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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/
  16. Database Declaration @Database( entities = [ Song::class, Album::class, Artist::class, ],

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

    version = 1 ) abstract class MusicDatabase : RoomDatabase() { abstract fun musicDao(): MusicDao }
  18. DatabaseProcessingStep.kt class DatabaseProcessingStep : XProcessingStep { override fun process( ...

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

    ): Set<XTypeElement> { val databases = elementsByAnnotation[Database::class.qualifiedName] ?.filterIsInstance<XTypeElement>() ?.mapNotNull { annotatedElement -> DatabaseProcessor(annotatedElement).process() } databases?.forEach { db -> // generate code } } }
  20. @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
  21. 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) }
  22. 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 } }
  23. 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!
  24. DatabaseWriter.kt fun create() : MusicDao { } MusicDao create() {

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

    create() { return new MusicDao_Impl(); } (Generated Code) Kotlin output Java output
  26. 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
  27. 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
  28. 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
  29. 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
  30. DatabaseWriter.kt (XPoet) private fun createFunction( codeLanguage: CodeLanguage, dao: Dao ):

    XFunSpec { return XFunSpec.builder(codeLanguage, "create", VisibilityModifier.PUBLIC).apply { }.build() }
  31. 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() }
  32. SQLite.kt package androidx.sqlite /** * Executes a single SQL statement

    that returns no values. */ fun SQLiteConnection.execSQL(sql: String) { prepare(sql).use { it.step() } }
  33. AutoMigrationWriter.kt public override fun migrate( ) { } @Override public

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

    @Override public void migrate( @NonNull final SQLiteConnection connection ) { } (Generated Code) Kotlin output Java output
  35. 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
  36. 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
  37. 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) ) ) }
  38. Compromise: “Java-ish Kotlin” public override fun listOfString(arg: List<String>): MyEntity {

    val _stringBuilder: StringBuilder = StringBuilder() ... } (Generated Code)
  39. Compromise: “Java-ish Kotlin” public override fun listOfString(arg: List<String>): 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)
  40. 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.
  41. DatabaseProcessorTest.kt val dbSrc = Source.kotlin( "MyDatabase.kt", """ import androidx.room.* @Database(entities

    = [MyEntity::class], version = 1) abstract class MyDatabase """.trimIndent() ) : RoomDatabase
  42. DatabaseProcessorTest.kt val dbSrc = Source.kotlin( ... ) val entitySrc =

    Source.java( "MyEntity", """ import androidx.room.* class MyEntity { @PrimaryKey long id; } """.trimIndent() )
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. Framework SQLite Android Common Native Android Native sqlite3.h android.database. SQLiteDatabase

    Android SQLite Driver Native SQLite Driver room-runtime sqlite-framework
  49. 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
  50. 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
  51. Maintaining Compatibility • Surveyed Room APIs • Move compatible APIs

    to common • Retain Android-only functionality
  52. 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
  53. Interfaces and Implementations // src/nativeMain/.../NativeSQLiteDriver.native.kt class NativeSQLiteDriver : SQLiteDriver {

    override fun open(fileName: String): SQLiteConnection = memScoped { val dbPointer = allocPointerTo<sqlite3>() 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
  54. 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
  55. Top-Level Declarations // src/commonMain/.../TableInfo.kt expect class TableInfo { override fun

    equals(other: Any?): Boolean override fun hashCode(): Int } Strategy
  56. 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
  57. 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
  58. Additional Problems • Heavy reliance on Executors, Runnables and Callables.

    • The use of reflection while accessing the generated database implementation.
  59. Reflection KMP fun createRoomDatabase() = Room.inMemoryDatabaseBuilder<MusicDatabase>( context = appContext ).build()

    @Generated(value = ["androidx.room.RoomProcessor"]) public class MusicDatabase_Impl : MusicDatabase() { // ... } Class.forName(databaseClass + "_Impl")
  60. Reflection KMP fun createRoomDatabase() = Room.inMemoryDatabaseBuilder<MusicDatabase>( 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() { // ... }
  61. 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
  62. 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.
  63. 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 ... ... ...
  64. 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 ... ... ... 🎉🎉
  65. ੿ܻ • 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