Slide 1

Slide 1 text

KMP 개발을 위한 알아두면 좋은 라이브러리 소개 / DI 프레임워크 찍먹하기 Incheon/Songdo 이상훈 | at Studio Product Engineer

Slide 2

Slide 2 text

저를 소개합니다! 이상훈 (Dora Lee) at Product Engineer @dsa28s [email protected]

Slide 3

Slide 3 text

INDEX 1. KMP에서 DI 사용하기 + DI 프레임워크 ‘koin’ 사용법 소개 2-1. KMP shared 모듈에서 사용하기 2-2. KMP shared 모듈에서 주입한 객체를 안드로이드에서 사용하기 2-3. KMP shared 모듈에서 주입한 객체를 iOS (Swift) 에서 사용하기 2. KMP 개발을 위한 카테고리별 라이브러리 소개 네트워킹, 로깅, 환경설정 / 데이터 저장, 권한, 하드웨어, 아키텍처 프레임워크, 크래시리포트, 보안, 파일, 직렬화/역직렬화, 시간, Firebase 총 12개의 카테고리에 맞는 유용한 라이브러리들 소개

Slide 4

Slide 4 text

KMP에서 DI 사용하기 + DI 프레임워크 ‘koin’ 사용법 소개 1.

Slide 5

Slide 5 text

Why using Dependency Injection? class AwesomeGDGSongdo constructor() { private val conference: Conference init { this.conference = new Conference() } public fun getSpeakers(): List { return this.conference.speakers() } }

Slide 6

Slide 6 text

Why using Dependency Injection? class AwesomeGDGSongdo constructor() { private val conference: Conference init { this.conference = new Conference() } public fun getSpeakers(): List { return this.conference.speakers() } } AwesomeGDGSongdo 클래스는 Conference를 의존성으로 가지게 됨

Slide 7

Slide 7 text

Why using Dependency Injection? class AwesomeGDGSongdo constructor() { private val conference: Conference init { this.conference = new Conference() } public fun getSpeakers(): List { return this.conference.speakers() } } 만약, Conference에 들어가는 요소가 달라지거나, Impl 종류가 달라지는 등의 수정이 생기면 → AwesomeGDGSongdo 클래스 또한 수정 필요

Slide 8

Slide 8 text

Why using Dependency Injection? class AwesomeGDGSongdo constructor( val conference: Conference ) { public fun getSpeakers(): List { return this.conference.speakers() } }

Slide 9

Slide 9 text

Why using Dependency Injection? class AwesomeGDGSongdo constructor( val conference: Conference ) { public fun getSpeakers(): List { return this.conference.speakers() } } 지금은 외부에서 conference를 개발자가 마음대로 주입하여 사용함 → 의존성 줄이기 가능

Slide 10

Slide 10 text

Why using Dependency Injection? 1. 단위 테스트 짜기 쉬워짐 2. 코드 가독성 증가 3. 코드 재활용성 증가 4. 객체 간 의존성 줄여줌 5. 유연하게 코드 작성 가능

Slide 11

Slide 11 text

Introducing ‘Insert Koin’ https://insert-koin.io/

Slide 12

Slide 12 text

Introducing ‘Insert Koin’ 안드로이드 뿐만 아니라 멀티플랫폼 환경에서도 DI(Dependency Injection)을 할 수 있게 해주는 라이브러리 멀티플랫폼 환경에서는 Hilt(Dagger) 등 안드로이드에서 주로 쓰이는 DI 라이브러리를 사용할 수 없음

Slide 13

Slide 13 text

Introducing ‘Insert Koin’ Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 14

Slide 14 text

Koin Method module single factory viewModel get bind

Slide 15

Slide 15 text

Koin Method module single factory viewModel get bind class ComponentA() class ComponentB(val componentA : ComponentA) val moduleA = module { // ComponentA 싱글톤으로 만들기 single { ComponentA() } } val moduleB = module { // Koin으로 만들어진 모듈로부터 ComponentA 가져오기 (ComponentB도 싱글톤) single { ComponentB(get()) } }

Slide 16

Slide 16 text

Koin Method module single factory viewModel get bind class ComponentA() class ComponentB(val componentA : ComponentA) val moduleA = module { // ComponentA 싱글톤으로 만들기 single { ComponentA() } } val moduleB = module { // Koin으로 만들어진 모듈로부터 ComponentA 가져오기 (ComponentB도 싱글톤) single { ComponentB(get()) } }

Slide 17

Slide 17 text

Koin Method module single factory viewModel get bind class ComponentA() class ComponentB(val componentA : ComponentA) val moduleA = module { // ComponentA 호출할 때마다 새로 생성 factory { ComponentA() } } val moduleB = module { // Koin으로 만들어진 모듈로부터 ComponentA 가져오기 (ComponentB도 싱글톤) single { ComponentB(get()) } }

Slide 18

Slide 18 text

Koin Method (Android Specific) module single factory viewModel get bind class ComponentA() class ComponentB(val componentA : ComponentA) val moduleA = module { // ComponentA 호출할 때마다 새로 생성 factory { ComponentA() } } val moduleB = module { // ExampleViewModel 모듈로 추가 viewModel { ExampleViewModel(get(), get()) } }

Slide 19

Slide 19 text

Koin Method module single factory viewModel get bind class ComponentA() class ComponentB(val componentA : ComponentA) val moduleA = module { // ComponentA 호출할 때마다 새로 생성 factory { ComponentA() } } val moduleB = module { // ExampleViewModel 모듈로 추가 (2개의 파라미터에 koin으로 주입된 모듈 가져오기) viewModel { ExampleViewModel(get(), get()) } }

Slide 20

Slide 20 text

Koin Method module single factory viewModel get bind // Service interface interface Service{ fun doSomething() } // Service Implementation class ServiceImpl() : Service{ fun doSomething() { ... } } val myModule = module { single { ServiceImpl() } bind Service::class }

Slide 21

Slide 21 text

사용법 :: Shared Module // platform Module (Shared) val platformModule = module { singleOf(::Platform) } // KMP Class Definition expect class Platform() { val name: String }

Slide 22

Slide 22 text

사용법 :: Shared Module // iOS actual class Platform actual constructor() { actual val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } // Android actual class Platform actual constructor() { actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}" }

Slide 23

Slide 23 text

사용법 :: Android fun appModule() = listOf(commonModule, platformModule) startKoin { androidContext(this@MainApplication) androidLogger() modules(appModule() + androidModule) } private val platform: Platform by inject()

Slide 24

Slide 24 text

사용법 :: iOS (Swift) Helper.kt fun initKoin() { startKoin { modules(appModule()) } }

Slide 25

Slide 25 text

사용법 :: iOS (Swift) MainApp.swift struct iOSApp: App { // KMM - Koin Call init() { HelperKt.doInitKoin() } var body: some Scene { WindowGroup { ContentView() } } }

Slide 26

Slide 26 text

사용법 :: iOS (Swift) MainApp.swift struct iOSApp: App { // KMM - Koin Call init() { HelperKt.doInitKoin() } var body: some Scene { WindowGroup { ContentView() } } } KMP에서 Swift로 빌드될 때 Kotlin에서 init~ 으로 시작할 경우, 키워드 충돌 방지를 위해 do~ 로 시작하게 됨 아까 initKoin으로 선언한 함수가 위 규칙으로 인해 doInitKoin 이 됨

Slide 27

Slide 27 text

사용법 :: iOS (Swift) :: 주입된 모듈 사용하기 PlatformModuleHelper.kt // iOS Only class PlatformModuleHelper : KoinComponent { private val platform : Platform by inject() fun name() : String = platform.name } ContentView.swift struct ContentView: View { // 만들었던 Helper 클래스 swift에서 불러오기 let platformName = PlatformModuleHelper().name() var body: some View { Text(platformName) } }

Slide 28

Slide 28 text

1. KMP 개발을 위한 알아두면 좋은 라이브러리 카테고리 별 소개 2.

Slide 29

Slide 29 text

Introducing Category 🌎 Networking 📋 Logging 📦 Storage / Preferences 🚧 Permission 📱Hardware 🏗 Architecture 💥 Crash Reporting 🔐 Security 📄 File (I/O) 🗃 Serializer ⏰ Date / Time Firebase

Slide 30

Slide 30 text

Introducing Library 🌎 Networking Incheon/Songdo

Slide 31

Slide 31 text

🌎 Networking ktorfit https://ktor.io/ https://github.com/Foso/Ktorfit

Slide 32

Slide 32 text

🌎 Networking ktorfit +

Slide 33

Slide 33 text

🌎 Networking ktorfit + =

Slide 34

Slide 34 text

🌎 Networking | 마이크로 서비스, 웹 애플리케이션 등을 만들기 위한 비동기식 프레임워크

Slide 35

Slide 35 text

🌎 Networking | 마이크로 서비스, 웹 애플리케이션 등을 만들기 위한 비동기식 프레임워크 Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 36

Slide 36 text

🌎 Networking | Client Example HelloWorld.kt package com.example.kmmktor import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* class HelloWorld { private val client = HttpClient() suspend fun greeting(): String { val response = client.get("https://google.com/") return response.bodyAsText() } }

Slide 37

Slide 37 text

🌎 Networking | Client Example 하지만, 보일러플레이트 코드도 많고 공통화 한다고 해도 통신 로직이 간결해지지 않음

Slide 38

Slide 38 text

🌎 Networking | Client Example 더더욱, 안드로이드 앱을 만들었던 개발자가 Retrofit을 경험했다면 ktor 순수 경험 자체는 좋지 않을 수 있음

Slide 39

Slide 39 text

🌎 Networking | + ktorfit = 🧡 ktor 클라이언트 코드를 KSP (Kotlin Symbol Processing)을 통해 안드로이드에서의 Retrofit 처럼 사용할 수 있게 해주는 라이브러리

Slide 40

Slide 40 text

🌎 Networking | + ktorfit = 🧡 ktor 클라이언트 코드를 KSP (Kotlin Symbol Processing)을 통해 안드로이드에서의 Retrofit 처럼 사용할 수 있게 해주는 라이브러리 Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 41

Slide 41 text

🌎 Networking | + ktorfit = 🧡 KtorfitExample.kt @Headers(["Content-Type: application/json"]) @GET("{user}/comments") suspend fun getComments( @Path("user") user: String, @Query("limit") limit: Int ): List

Slide 42

Slide 42 text

🌎 Networking | + ktorfit = 🧡 ExampleKtorfit.kt interface ExampleApi { @GET("people/1/") suspend fun getPerson(): String } val ktorfit = Ktorfit.Builder().baseUrl("https://your-api-server.dev/api/").build() val exampleApi = ktorfit.create() // 실제 사용 val response = exampleApi.getPerson() println(response)

Slide 43

Slide 43 text

Introducing Library 📋 Logging Incheon/Songdo

Slide 44

Slide 44 text

📋 Logging Kermit the log https://github.com/AAkira/Napier https://github.com/touchlab/Kermit

Slide 45

Slide 45 text

📋 Logging Kermit the log AAkira/Napier touchlab/Kermit 모두 멀티플랫폼에서 로깅을 위한 라이브러리

Slide 46

Slide 46 text

📋 Logging Kermit the log Info, warning, error, debug 등 severity에 따라 로깅 기본 태그 지정 Xcode, OS 로깅 등 각 플랫폼에 맞는 빌트인 Logger 지원

Slide 47

Slide 47 text

📋 Logging Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ Kermit the log

Slide 48

Slide 48 text

Introducing Library 📦 Storage / Preferences Incheon/Songdo

Slide 49

Slide 49 text

📦 Storage / Preferences https://developer.android.com/jetpack /androidx/releases/datastore https://github.com/cashapp/sqldelight androidx.datastore SQLDelight

Slide 50

Slide 50 text

📦 Storage / Preferences | DataStore https://developer.android.com/jetpack /androidx/releases/datastore androidx.datastore 여러분이 아시는 안드로이드의 데이터스토어가 맞습니다

Slide 51

Slide 51 text

📦 Storage / Preferences | DataStore https://developer.android.com/jetpack /androidx/releases/datastore androidx.datastore 1.1.0-alpha03 버전부터 Kotlin Multiplatform을 공식 지원합니다

Slide 52

Slide 52 text

📦 Storage / Preferences | DataStore https://developer.android.com/jetpack /androidx/releases/datastore androidx.datastore Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ❌ ❌ ❌ ✅ ❌

Slide 53

Slide 53 text

📦 Storage / Preferences | DataStore https://developer.android.com/jetpack /androidx/releases/datastore androidx.datastore ProtoDataStore Protocol Buffer 기반 Type-safety Custom Entity based preferences PreferencesDataStore key/value 기반의 preferences

Slide 54

Slide 54 text

📦 Storage / Preferences | DataStore [commonMain] DataStore.kt fun createDataStore( producePath: () -> String, ): DataStore = PreferenceDataStoreFactory.createWithPath( corruptionHandler = null, migrations = emptyList(), produceFile = { producePath().toPath() }, ) const val dataStoreFileName = "devfest.preferences_pb"

Slide 55

Slide 55 text

📦 Storage / Preferences | DataStore [androidMain] DataStore.kt fun dataStore(context: Context): DataStore = createDataStore( producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath } )

Slide 56

Slide 56 text

📦 Storage / Preferences | DataStore [iosMain] DataStore.kt fun dataStore(): DataStore = createDataStore( producePath = { val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( directory = NSDocumentDirectory, inDomain = NSUserDomainMask, appropriateForURL = null, create = false, error = null, ) requireNotNull(documentDirectory).path + "/$dataStoreFileName" } )

Slide 57

Slide 57 text

📦 Storage / Preferences | DataStore [commonMain] Example.kt val NOTI_ENABLED = booleanPreferencesKey("is_notification_enabled") suspend fun updateNotificationEnabledStatus(isEnabled: Boolean) { dataStore.edit { preferences -> preferences[NOTI_ENABLED] = isEnabled } } val notificationEnabledStatusFlow: Flow = dataStore.data .map { preferences -> preferences[NOTI_ENABLED] }

Slide 58

Slide 58 text

📦 Storage / Preferences | SQLDelight SQLite (or AnotherDB) for Multiplatform SQL 쿼리문을 작성하면 해당 쿼리에 맞는 Entity와 DB로부터 Read, Write 하기 위한 모든 코드가 자동으로 생성됨. 또 IDE (ex. Android Studio)에서 SQLDelight가 생성한 API에 대해서 자동완성, Syntax Highlight 등 지원

Slide 59

Slide 59 text

📦 Storage / Preferences | SQLDelight Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 60

Slide 60 text

📦 Storage / Preferences | SQLDelight TableDefinition.sq CREATE TABLE hockey_player ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, number INTEGER NOT NULL );

Slide 61

Slide 61 text

📦 Storage / Preferences | SQLDelight TableDefinition.sq CREATE TABLE hockey_player ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, number INTEGER NOT NULL );

Slide 62

Slide 62 text

📦 Storage / Preferences | SQLDelight src/main/sqldelight/com/example/sqldelight/hockey/data/Player.sq CREATE TABLE hockeyPlayer ( player_number INTEGER PRIMARY KEY NOT NULL, full_name TEXT NOT NULL ); CREATE INDEX hockeyPlayer_full_name ON hockeyPlayer(full_name); INSERT INTO hockeyPlayer (player_number, full_name) VALUES (7, 'Dora Lee'); [스키마 정의하기]

Slide 63

Slide 63 text

📦 Storage / Preferences | SQLDelight [commonMain] Database.kt import com.example.Database expect class DriverFactory { fun createDriver(): SqlDriver } fun createDatabase(driverFactory: DriverFactory): Database { val driver = driverFactory.createDriver() return Database(driver) } [드라이버 공통 인터페이스]

Slide 64

Slide 64 text

📦 Storage / Preferences | SQLDelight [androidMain] Database.kt actual class DriverFactory(private val context: Context) { actual fun createDriver(): SqlDriver { return AndroidSqliteDriver(Database.Schema, context, "test.db") } } [드라이버 - 안드로이드]

Slide 65

Slide 65 text

📦 Storage / Preferences | SQLDelight [nativeMain] Database.kt actual class DriverFactory { actual fun createDriver(): SqlDriver { return NativeSqliteDriver(Database.Schema, "test.db") } } [드라이버 - 네이티브 플랫폼들]

Slide 66

Slide 66 text

📦 Storage / Preferences | SQLDelight src/main/sqldelight/com/example/sqldelight/hockey/data/Player.sq selectAll: SELECT * FROM hockeyPlayer; insert: INSERT INTO hockeyPlayer(player_number, full_name) VALUES (?, ?); insertFullPlayerObject: INSERT INTO hockeyPlayer(player_number, full_name) VALUES ?; [사용할 쿼리 정의]

Slide 67

Slide 67 text

📦 Storage / Preferences | SQLDelight Example.kt fun dbQueryExample(driver: SqlDriver) { val database = Database(driver) val playerQueries: PlayerQueries = database.playerQueries println(playerQueries.selectAll().executeAsList()) // [HockeyPlayer(7, "Dora Lee")] playerQueries.insert(player_number = 10, full_name = "Dora Lee 2222") println(playerQueries.selectAll().executeAsList()) // [HockeyPlayer(7, "Dora Lee"), HockeyPlayer(10, "Dora Lee 2222")] val player = HockeyPlayer(1, "Dora Lee 333333") playerQueries.insertFullPlayerObject(player) }

Slide 68

Slide 68 text

Introducing Library 🚧 Permission Incheon/Songdo

Slide 69

Slide 69 text

🚧 Permission https://github.com/icerockdev/moko-permissions

Slide 70

Slide 70 text

🚧 Permission | 멀티플랫폼 환경에서 각 플랫폼 별 Runtime Permission을 간단한 API로 쉽게 핸들링 할 수 있는 라이브러리

Slide 71

Slide 71 text

🚧 Permission | Supported Permissions Camera: Permission.CAMERA Gallery: Permission.GALLERY Storage read: Permission.STORAGE Storage write: Permission.WRITE_STORAGE Fine location: Permission.LOCATION Coarse location: Permission.COARSE_LOCATION Remote notifications: Permission.REMOTE_NOTIFICATION Audio recording: Permission.RECORD_AUDIO Bluetooth LE: Permission.BLUETOOTH_LE Bluetooth Scan: Permission.BLUETOOTH_SCAN Bluetooth Connect: Permission.BLUETOOTH_CONNECT Bluetooth Advertise: Permission.BLUETOOTH_ADVERTISE

Slide 72

Slide 72 text

🚧 Permission | Android macOS iOS watchOS tvOS Windows Linux Web ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌

Slide 73

Slide 73 text

Introducing Library 📱 Hardware Incheon/Songdo

Slide 74

Slide 74 text

📱 Hardware https://github.com/line/abc-kmm-notifications https://github.com/line/abc-kmm-location https://github.com/icerockdev/moko-biometry

Slide 75

Slide 75 text

📱 Hardware https://github.com/juullabs/kable kable https://github.com/ln-12/multiplatform-conne ctivity-status multiplatform- connectivity-status

Slide 76

Slide 76 text

📱 Hardware 푸시 알림 (FCM, APNs)을 멀티플랫폼 환경에서 수신 / 핸들링 할 수 있는 라이브러리 디바이스의 현재 위치(실시간 포함)를 멀티플랫폼 환경에서 가져올 수 있는 라이브러리 디바이스의 생체 인증(지문, FaceID, TouchID)을 멀티플랫폼 환경에서 사용할 수 있게 해주는 라이브러리

Slide 77

Slide 77 text

📱 Hardware Bluetooth Low Enerey (BLE) 장치를 멀티플랫폼 환경에서 스캔하고 통신하게 해주는 라이브러리 kable 멀티플랫폼 환경에서 실시간 네트워크 연결 상태를 핸들링 할 수 있는 라이브러리 multiplatform- connectivity-status

Slide 78

Slide 78 text

Introducing Library 🏗 Architecture Incheon/Songdo

Slide 79

Slide 79 text

🏗 Architecture https://github.com/arkivanov/Decompose https://github.com/orbit-mvi/orbit-mvi Orbit Multiplatform

Slide 80

Slide 80 text

🏗 Architecture 라이브러리 자체 제공 라우팅 기능 + 플러그인 형태의 UI를 통해 비즈니스 로직 (ex. Flutter의 BLoC)을 작성해주는 라이브러리 Orbit Multiplatform 멀티플랫폼 환경에서 MVI / Redux 같은 형태로 뷰모델을 작성할 수 있게 해주는 라이브러리 라이브러리 문서 상으로는 MVVM+ 라고 주장(?) 중

Slide 81

Slide 81 text

🏗 Architecture

Slide 82

Slide 82 text

🏗 Architecture | Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ❌ ❌ ✅

Slide 83

Slide 83 text

🏗 Architecture | 컴포넌트 트리구조

Slide 84

Slide 84 text

🏗 Architecture | 플러그인 형태의 UI

Slide 85

Slide 85 text

🏗 Architecture | 컴포넌트 최종 구성요소

Slide 86

Slide 86 text

🏗 Architecture | 싱글 컴포넌트 예시

Slide 87

Slide 87 text

🏗 Architecture | SingleComponentExample.kt interface ListComponent { val model: Value fun onItemClicked(item: String) data class Model( val items: List, ) }

Slide 88

Slide 88 text

🏗 Architecture | SingleComponentExample.kt class DefaultListComponent( componentContext: ComponentContext, private val onItemSelected: (item: String) -> Unit, ) : ListComponent { override val model: Value = MutableValue(Model(items = List(100) { "Item $it" })) override fun onItemClicked(item: String) { onItemSelected(item) } }

Slide 89

Slide 89 text

🏗 Architecture | SingleComponentExampleUI.kt @Composable fun ListContent(component: ListComponent, modifier: Modifier = Modifier) { val model by component.model.subscribeAsState() LazyColumn { items(items = model.items) { item -> Text( text = item, modifier = Modifier.clickable { component.onItemClicked(item = item) }, ) } } }

Slide 90

Slide 90 text

🏗 Architecture | SingleComponentExampleUI.swift struct DetailsView: View { private let list: ListComponent @StateValue private var model: ListComponentModel init(_ list: ListComponent) { self.list = list _model = StateValue(list.model) } var body: some View { List(model.items, ...) { item in // Display the item } } }

Slide 91

Slide 91 text

🏗 Architecture | 네비게이션 컴포넌트 예시

Slide 92

Slide 92 text

🏗 Architecture | NavigationComponentExample.kt interface RootComponent { val stack: Value> // iOS는 여러개의 화면을 동시에 pop 할 수 있기 때문에 특정 index 만큼 pop 할 수 있도록 처리 fun onBackClicked(toIndex: Int) // Child Navigation 모두 정의 sealed class Child { class ListChild(val component: ListComponent) : Child() class DetailsChild(val component: DetailsComponent) : Child() } }

Slide 93

Slide 93 text

🏗 Architecture | NavigationComponentExample.kt class DefaultRootComponent( componentContext: ComponentContext, ) : RootComponent, ComponentContext by componentContext { private val navigation = StackNavigation() // TODO: Navigation Definition @Serializable private sealed interface Config { @Serializable data object List : Config @Serializable data class Details(val item: String) : Config } }

Slide 94

Slide 94 text

🏗 Architecture | NavigationComponentExample.kt class DefaultRootComponent(...) : RootComponent, ComponentContext by componentContext { override val stack: Value> = childStack( source = navigation, serializer = Config.serializer(), initialConfiguration = Config.List, handleBackButton = true, childFactory = ::child, ) override fun onBackClicked(toIndex: Int) { navigation.popTo(index = toIndex) } }

Slide 95

Slide 95 text

🏗 Architecture | NavigationComponentExample.kt class DefaultRootComponent(...) : RootComponent, ComponentContext by componentContext { private fun listComponent(componentContext: ComponentContext): ListComponent = DefaultListComponent( componentContext = componentContext, onItemSelected = { item: String -> // Supply dependencies and callbacks navigation.push(Config.Details(item = item)) // Push the details component }, ) private fun detailsComponent(componentContext: ComponentContext, config: Config.Details): DetailsComponent = DefaultDetailsComponent( componentContext = componentContext, item = config.item, // Supply arguments from the configuration onFinished = navigation::pop, // Pop the details component ) }

Slide 96

Slide 96 text

🏗 Architecture | NavigationComponentExample.kt class DefaultRootComponent(...) : RootComponent, ComponentContext by componentContext { private fun child(config: Config, componentContext: ComponentContext): RootComponent.Child = when (config) { is Config.List -> ListChild(listComponent(componentContext)) is Config.Details -> DetailsChild(detailsComponent(componentContext, config)) } }

Slide 97

Slide 97 text

🏗 Architecture | NavigationComponentExampleUI.kt @Composable fun RootContent(component: RootComponent, modifier: Modifier = Modifier) { Children( stack = component.stack, modifier = modifier, animation = stackAnimation(fade() + scale()), ) { when (val child = it.instance) { is ListChild -> ListContent(component = child.component) is DetailsChild -> DetailsContent(component = child.component) } } }

Slide 98

Slide 98 text

Orbit Multiplatform 🏗 Architecture

Slide 99

Slide 99 text

🏗 Architecture | Android macOS iOS watchOS tvOS Windows Linux Web ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌

Slide 100

Slide 100 text

🏗 Architecture | Reference: [Medium - Matthew Dolan] Top Android MVI libraries in 2021 https://appmattus.medium.com/top-android-mvi-libraries-in-2021-de1afe890f27

Slide 101

Slide 101 text

🏗 Architecture | 코틀린 코루틴 100% Lifecycle-Safety Flow Collection [Android] SavedStateHandle 대응 완료 테스트 API 지원 [Android] RxJava + LiveData 지원

Slide 102

Slide 102 text

🏗 Architecture | What’s Orbit? State + SideEffect -> Managed by ContainerHost Android ViewModel 안드로이드에서는 ViewModel이 Orbit의 ContainerHost 역할이 됨

Slide 103

Slide 103 text

🏗 Architecture | State.kt data class CalculatorState( val total: Int = 0 ) sealed class CalculatorSideEffect { data class Toast(val text: String) : CalculatorSideEffect() }

Slide 104

Slide 104 text

🏗 Architecture | CalculatorViewModel.kt class CalculatorViewModel: ContainerHost, ViewModel() { override val container = container(CalculatorState()) fun add(number: Int) = intent { postSideEffect(CalculatorSideEffect.Toast("${state.total}! + $number = ?")) reduce { state.copy(total = state.total + number) } } }

Slide 105

Slide 105 text

🏗 Architecture | CalculatorScreen.kt @Composable fun CalculatorScreen(viewModel: CalculatorViewModel) { val state by viewModel.collectAsState() viewModel.collectSideEffect { when(it) { CalculatorSideEffect.Toast -> Toast.makeText(...) } } Calculator( state = state ) { Text("${state.total}") } }

Slide 106

Slide 106 text

🏗 Architecture 멀티플랫폼 환경에서 MVVM(Model, View, ViewModel) 아키텍처 구성요소를 제공해주는 라이브러리 번외.. 당연히 MVVM 멀티플랫폼 라이브러리도 있습니다.

Slide 107

Slide 107 text

🏗 Architecture 멀티플랫폼 환경에서 MVVM(Model, View, ViewModel) 아키텍처 구성요소를 제공해주는 라이브러리 Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 108

Slide 108 text

Introducing Library 💥 Crash Reporting Incheon/Songdo

Slide 109

Slide 109 text

💥 Crash Reporting https://sentry.io/welcome https://github.com/touchlab/CrashKiOS https://github.com/icerockdev/moko-crash-reporting CrashKiOS

Slide 110

Slide 110 text

💥 Crash Reporting CrashKiOS 오류 모니터링 플랫폼 ‘Sentry’의 Multiplatform SDK 각 OS별 SDK도 지원하지만 Kotlin Multiplatform SDK도 공식 지원 Firebase Crashlytics와 Bugsnag를 지원하는 멀티플랫폼 크래시 리포트 라이브러리 Firebase Crashlytics를 지원하고, Fatal Error / Non-Fatal Error로 나눠 세부적인 크래시 로깅을 할 수 있는 라이브러리

Slide 111

Slide 111 text

💥 Crash Reporting Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ❌ ❌ ✅

Slide 112

Slide 112 text

💥 Crash Reporting Android macOS iOS watchOS tvOS Windows Linux Web ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ CrashKiOS

Slide 113

Slide 113 text

Introducing Library 🔐 Security Incheon/Songdo

Slide 114

Slide 114 text

🔐 Security https://github.com/whyoleg/ cryptography-kotlin https://github.com/ionspin/ kotlin-multiplatform-libsodium cryptography-kotlin kotlin-multiplatform -libsodium

Slide 115

Slide 115 text

🔐 Security 크로스플랫폼 Low Level 암호화 라이브러리인 libsodium 을 멀티플랫폼에 Wrapping 해놓은 라이브러리 libsodium 함수를 바인딩 해놓은 것이라서 함수 호출해서 low-level API 직접 호출 가능 cryptography-kotlin kotlin-multiplatform -libsodium OpenSSL, WebCrypto 같은 암호화 라이브러리를 멀티플랫폼 환경에 Wrapping 해놓은 라이브러리 다양한 암호화 관련 함수와 Secure Random, SHA~, AES, RSA, ECDSA 등을 지원함

Slide 116

Slide 116 text

🔐 Security Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ❌ ❌ ❌ ✅ ✅ cryptography-kotlin

Slide 117

Slide 117 text

🔐 Security Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ kotlin-multiplatform -libsodium

Slide 118

Slide 118 text

Introducing Library 📄 File (I/O) Incheon/Songdo

Slide 119

Slide 119 text

📄 File (I/O) https://github.com/square/okio okio

Slide 120

Slide 120 text

📄 File (I/O) java.io 와 java. nio를 보완하여 데이터 접근, 저장, 처리를 쉽게 해주는 멀티플랫폼 라이브러리 네트워킹 라이브러리인 OkHttp에도 사용중임 물론, Compression, Concurrency 등 JVM에서만 작동하는 API들도 존재하지만 Byte를 Hex로 바꾼다던가 (반대도 가능), 인코딩, 해싱, 파일 시스템 등 많은 편의 기능은 멀티플랫폼 환경에서도 작동함 okio

Slide 121

Slide 121 text

📄 File (I/O) okio Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 122

Slide 122 text

Introducing Library 🗃 Serializer Incheon/Songdo

Slide 123

Slide 123 text

🗃 Serializer https://github.com/Kotlin/kotlinx.serialization kotlinx.serialization

Slide 124

Slide 124 text

🗃 Serializer JVM, Android 환경 뿐만 아니라 코틀린 멀티플랫폼 환경에서도 Json, Protocol Buffer 등의 포맷을 Serialization 해주는 라이브러리 Kotlin 언어 자체에 대한 특정 (ex. default value, nullable) 등을 모두 고려하여 설계됨 @Serializable 어노테이션이 있는 클래스만 직렬화 함 → 개발자 실수 등으로 인해 누락되거나 지원하지 않는 타입에 대해서는 컴파일 시점에서 오류를 발생시켜 버그를 사전에 방지할 수 있음 Reflection을 사용하지 않으며, kotlin compiler 플러그인 레벨에서 Serialization 관련 코드를 생성함 kotlinx.serialization

Slide 125

Slide 125 text

🗃 Serializer KSerializerExample.kt import kotlinx.serialization.* import kotlinx.serialization.json.* @Serializable data class Project(val name: String, val language: String) fun main() { val data = Project("kotlinx.serialization", "Kotlin") val string = Json.encodeToString(data) println(string) // {"name":"kotlinx.serialization","language":"Kotlin"} val obj = Json.decodeFromString(string) println(obj) // Project(name=kotlinx.serialization, language=Kotlin) }

Slide 126

Slide 126 text

🗃 Serializer kotlinx.serialization Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 127

Slide 127 text

Introducing Library ⏰ Date / Time Incheon/Songdo

Slide 128

Slide 128 text

⏰ Date / Time https://github.com/Kotlin/kotlinx-datetime kotlinx.datetime

Slide 129

Slide 129 text

⏰ Date / Time JVM, Android 환경 뿐만 아니라 코틀린 멀티플랫폼 환경에서도 날짜, 시간, 시간대(TimeZone) 등을 쉽게 계산하고 표현할 수 있게 해주는 라이브러리 특정 시간에 시간을 더하기 / 빼기 등의 연산을 할 수 있고, 다양한 시간대로 변환도 할 수 있으며, 시간차이도 구할 수 있는 등 시간 관련하여 유용한 함수들이 제공됨 kotlinx.datetime

Slide 130

Slide 130 text

⏰ Date / Time ConvertExample.kt "2010-06-01T22:19:44.475Z".toInstant() "2010-06-01T22:19:44".toLocalDateTime() "2010-06-01".toLocalDate() "12:01:03".toLocalTime() "12:0:03.999".toLocalTime() TimeCalcExample.kt val now = Clock.System.now() val systemTZ = TimeZone.currentSystemDefault() val tomorrow = now.plus(2, DateTimeUnit.DAY, systemTZ) val threeYearsAndAMonthLater = now.plus(DateTimePeriod(years = 3, months = 1), systemTZ)

Slide 131

Slide 131 text

⏰ Date / Time kotlinx.datetime Android macOS iOS watchOS tvOS Windows Linux Web ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Slide 132

Slide 132 text

Introducing Library Firebase Incheon/Songdo

Slide 133

Slide 133 text

Firebase 공식적인 Firebase First-party 라이브러리는 없음

Slide 134

Slide 134 text

Firebase 대신 Third-party 라이브러리는 있음!

Slide 135

Slide 135 text

https://github.com/GitLiveApp/firebase-kotlin-sdk Firebase Kotlin SDK Firebase by GitLiveApp

Slide 136

Slide 136 text

Firebase Android SDK + Firebase iOS SDK를 하나의 Kotlin Multiplatform 환경에 Wrapping 해놓은 라이브러리 Firebase Firebase Kotlin SDK by GitLiveApp

Slide 137

Slide 137 text

Firebase

Slide 138

Slide 138 text

Firebase 비공식 라이브러리이기 때문에 아직 지원하지 않는 기능 + API도 존재 하지만 메이저 한 기능은 사용할 수 있는 수준 하지만, Crashlytics, FCM은 아직 구현되지 않았기 때문에 다른 라이브러리 혹은 직접 구현이 필요함

Slide 139

Slide 139 text

Android macOS iOS watchOS tvOS Windows Linux Web ✅ ❌ ✅ ❌ ❌ ❌ ❌ ❌ Firebase Kotlin SDK by GitLiveApp Firebase

Slide 140

Slide 140 text

KMP 개발을 위한 알아두면 좋은 라이브러리 소개 / DI 프레임워크 찍먹하기 Incheon/Songdo 이상훈 | at Studio Product Engineer