Slide 1

Slide 1 text

Kotlin Language Features in 2.0 and Beyond (Michail Zarečenskij) @dalinaum Coupang Pay

Slide 2

Slide 2 text

Copyright © 2024 Coupang, Inc. All right reserved. All Coupang trademarks and Coupang logos and service marks used are registered in the United States and other countries, Coupang, inc. And/or the property of its affiliates (collectively referred to as “Coupang”). Other companies mentioned herein are mentioned for identification purposes only, and Coupang acknowledges that the name of the company used may be a registered trademark of the company and that company alone has exclusive ownership of the trademark. The information contained herein is based on the author's personal experience as an executive and employee and does not represent Coupang's views or opinions. Coupang has not verified the adequacy, fairness, accuracy, or stability of the information contained herein, nor does it make any representations about it

Slide 3

Slide 3 text

Intro and Format - 코틀린 여정: 과거 기능들 - 코틀린의 현재 상태: 2.0의 기능들 - 나아갈 길: 향후 기능들

Slide 4

Slide 4 text

역사 코틀린 1.0은 2016년 2월 15일에 릴리스하였다.

Slide 5

Slide 5 text

역사: 1.0 이후의 기능들 ● 멀티플랫폼 프로젝트 ● 코루틴 ● 인라인 밸류 클래스 ● 트레일링 콤마 ● 함수형 인터페이스 ● 위임 프로퍼티 ● 바운드 호출가능한 레퍼런스 ● 람다에서 비구조화 ● 어노테이션의 배열 리터럴 ● 지역 지연 초기화 변수 ● 옵트인 어노테이션 ● 확실히 널이 아닌 타입 ● 서브젝트가 있는 when ● 어노테이션 클래스의 인스턴스화 ● … ● 타입 별칭 ● 실드 클래스와 인터페이스 ● 계약 ● when 안에 break/continue ● 철저한 when ● 빌더 추론 ● ..< 연산자 ● 데이터 객체 ● 효율적인Enum.entries

Slide 6

Slide 6 text

What is Kotlin? ● 코틀린은 언어이고 하고, ● Kotlin/JVM, Kotlin/Native, Kotlin/JS, Kotlin/WASM ● 코틀린 스크립트, Gradle Kotlin DSL ● 안드로이드와 서버사이드 코틀린 ● 도구: IntelliJ IDEA, Android Studio 확장, Fleet ● 코틀린 라이브러리: 코루틴, 직렬화, Kover… ● 컴파일러 도구: Compose, KSP, Power Assert, Arrow 플러그인…

Slide 7

Slide 7 text

새 코틀린 컴파일러는 왜 필요한가요? ● 언어의 몇몇 기능은 코틀린에서 예상하지 못하게 추가됨 ○ 컴파일러를 유지보수하고 발전하기가 어려움 ● 컴파일러 플러그인과 IDE의 상호작용 ○ 많은 임기 응변의 해결법, 엄격하지 않은 계약 관계, 안정된 API의 부재 ● 컴파일 시간 성능

Slide 8

Slide 8 text

코틀린 2.0 ● 코틀린 2.0 출시 ● 여러 서브 시스템에서 80개 이상의 기능 ● 언어 측면에서 25개 기능과 작은 향상들 ● 여전히, 성능과 정확성이 최우선

Slide 9

Slide 9 text

일반적인 컴파일러 변화는? ● 프론트엔드 중간 표현, Frontend Intermediate Representation (FIR) ○ 언어 구성은 더 단순한 구성으로 나뉩니다. ● 분석에 있어 단계별 접근 ○ Import, 어노테이션, 타입등의 언어 구성에 대한 개별적인 단계 ○ IDE와 컴파일러 플러그인을 위한 확장 ● 새로운 제어 흐름 엔진, 향상된 타입 추론과 레졸루션 ○ 코드를 통해 데이터 흐름 정보를 전달

Slide 10

Slide 10 text

사용자가 알만한 변화는?

Slide 11

Slide 11 text

프론트엔드 중간 표현, Frontend Intermediate Representation 코틀린의 힘은 구성에서 온다.

Slide 12

Slide 12 text

fun f(mList: MutableList) { mList[0] = mList[0] + 1 } Long 정수 리터럴 타입 ● Long과 정수 리터럴 타입의 조합 프론트엔드 중간 표현

Slide 13

Slide 13 text

프론트엔드 중간 표현 ● 연산자와 산술 변환의 조합 Error: 1L가 필요 fun f(mList: MutableList) { mList[0] += 1 // Kotlin 1.x에서 에러 }

Slide 14

Slide 14 text

프론트엔드 중간 표현 ● 연산자와 산술 변환의 조합 디슈거링 결과: mList.set(0, mList.get(0).plus(1)) fun f(mList: MutableList) { mList[0] += 1 // OK in 2.0 }

Slide 15

Slide 15 text

프론트엔드 중간 표현 ● 널 가능 연산자 호출의 조합 class Box(val mList: MutableList) fun g(box: Box?) { box?.mList[0] += 1 // Error in 1.x box?.mList[0] += 1L // Error in 1.x }

Slide 16

Slide 16 text

프론트엔드 중간 표현 Desugared into: box .run { mList.set(0, mList.get(0).plus(1)) } ● 널 가능 연산자 호출의 조합 class Box(val mList: MutableList) fun g(box: Box?) { box?.mList[0] += 1 // OK in 2.0 }

Slide 17

Slide 17 text

프론트엔드 중간 표현 ● 코틀린의 힘은 구성에서 온다. ○ 연산자 ○ 위임 프로퍼티 ○ 확장 함수 ○ …

Slide 18

Slide 18 text

새 제어 흐름 엔진 - 새 제어 흐름 엔진… 또는 스마트 캐스트의 추가! - KT-7186 Smart cast for captured variables inside changing closures of inline functions - KT-4113 Smart casts for properties to not-null functional types at invoke calls - KT-25747 DFA variables: propagate smart cast results from local variables - KT-1982 Smart cast to a common supertype of subject types after || (OR operator) - …

Slide 19

Slide 19 text

변수로부터 스마트 캐스트 class Cat { fun purr() { println("Purr purr") } } fun petAnimal(animal: Any) { if (animal is Cat) { animal.purr() } }

Slide 20

Slide 20 text

변수로부터 스마트 캐스트 class Cat { fun purr() { println("Purr purr") } } fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() // Error in Kotlin 1.x } } 1.x: 변수는 어떤 자료 흐름 정보를 가지지 않는다

Slide 21

Slide 21 text

변수로부터 스마트 캐스트 class Cat { fun purr() { println("Purr purr") } } fun petAnimal(animal: Any) { val val isCat = animal is Cat if (isCat) { animal.purr() // OK in 2.0 } } 2.0: 합성 데이터 흐름 변수가 스마트 캐스트에 관한 정보를 전파한다.

Slide 22

Slide 22 text

제약을 포함하여 모든 스마트 캐스트에 대한 일반 규칙이 작동한다. class Card(val holder: String?) fun foo(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } 변수로부터 스마트 캐스트

Slide 23

Slide 23 text

class Card(val holder: String?) fun foo(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } Any -> Card String? -> String String으로 스마트캐스트 제약을 포함하여 모든 스마트 캐스트에 대한 일반 규칙이 작동한다. 변수로부터 스마트 캐스트

Slide 24

Slide 24 text

인라인 람다의 클로져 안에서 스마트 캐스트 fun indexOfMax(a: IntArray): Int? { var maxI: Int? = null a.forEachIndexed { i, value > // 1.x: ‘Int’로 스마크 캐스트가 불가능, // 'maxI'은 클로즈 변경에 캡쳐되는 지역 변수임 if (maxI == null | a[maxI!!] <= value) { maxI = i } } return maxI }

Slide 25

Slide 25 text

인라인 람다의 클로져 안에서 스마트 캐스트 fun indexOfMax(a: IntArray): Int? { var maxI: Int? = null a.forEachIndexed { i, value > // 1.x: ‘Int’로 스마크 캐스트가 불가능, // 'maxI'은 클로즈 변경에 캡쳐되는 지역 변수임 if (maxI == null | a[maxI!!] <= value) { maxI = i } } return maxI }

Slide 26

Slide 26 text

인라인 람다의 클로져 안에서 스마트 캐스트 2.0은 인라인 함수를 암묵적으로 callInPlace 계약이 있는 것 처럼 취급합니다. fun indexOfMax(a: IntArray): Int? { var maxI: Int? = null a.forEachIndexed { i, value > // 2.0: OK if (maxI == null | a[maxI!!] <= value) { maxI = i } } return maxI }

Slide 27

Slide 27 text

‘||’뒤의 스마트 캐스트 interface Status { fun signal() } interface Ok : Status interface Postponed : Status interface Declined : Status

Slide 28

Slide 28 text

‘||’뒤의 스마트 캐스트 fun foo(signalStatus: Any) { if (signalStatus is Postponed || signalStatus is Declined) { // signalStatus는 Any로 추론 됨 signalStatus.signal() // Error } }

Slide 29

Slide 29 text

‘||’뒤의 스마트 캐스트: 공통 상위 타입으로 병합 fun foo(signalStatus: Any) { if (signalStatus is Postponed || signalStatus is Declined) { // signalStatus는 Status로 추론 됨 signalStatus.signal() // 2.0부터 OK } }

Slide 30

Slide 30 text

더 많은 “bugfixes” ● KT-24901 No smart cast for `when` with early return ● KT-13650 Right-hand side of a safe assignment is not always evaluated, which can fool smart-casts ● KT-18130 Smart cast can be broken by expression in string template ● KT-22454 Unsound smartcast in nested loops with labeled break from while-true ● KT-17694 Smart cast impossible on var declared in init block with a secondary constructor ● KT-56867 Green in K1 -> red in K2 for unsound code. `catch_end` to `good_finally` data flow ● KT-26148 No smartcasts when not-null assertion or not-null assignment in lambda (contract functions with EXACTLY_ONCE or AT_LEAST_ONCE effects) ● KT-23249 Inconsistent union type between platform type and non-platform type in K1. Fixed in K2 ● KT-27261 Contracts for infix functions don't work (for receivers and parameters) ● KT-30507 Unsound smartcast if null assignment inside index place and plusAssign/minusAssign is used ● KT-52424 ClassCastException: Wrong smartcast to Nothing? with if-else in nullable lambda parameter ● KT-37838 Support smart cast for inner/nested contracts ● KT-30756 No smartcast if elvis operator as a smartcast source in while or do-while is used as the last statement ● KT-53802 No smartcast after a while (true) infinite loop with break ● …

Slide 31

Slide 31 text

다음은 무엇인가요? - 멀티플랫폼: 스위프트 익스포트, 안정된 klib 포맷, Kotlin/Native 향상 - 도구: 앰퍼 그래들 프로젝트 격리, 노트북, 빌드 리포트 - 라이브러리: kotlinx-datetime, kotlinx-io, kotlinx-kover and others - 컴파일러: Kotlin/Wasm, 향상된 자바 상호호환, 타겟 전체의 인라인 문맥 통합 - 언어 자체…

Slide 32

Slide 32 text

왜 자료 흐름 프레임워크에 중점을 두나요? - 제어 흐름을 기술하는 것은 개발자의 주요 업무 - 스마트 캐스트는 인지 부하를 줄여준다 - 추가적인 언어 구성은 없음 - 점진적인 확장 가능

Slide 33

Slide 33 text

데이터 인식과 비구조화 - 가드: 바인딩 없는 패턴 매칭 - 문맥 인식 레졸루션 - 데이터 클래스 향상 - 이름 기반 비구조화 - 일관된 데이터 클래스 복사 가시성 - 일반화된 ADT - 이펙트 시스템 기능

Slide 34

Slide 34 text

@Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence, id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } val searchPanel = selectedSearchPanel() when { searchPanel is SearchPanel.NewsPanel && !searchPanel.isBlocked -> item { NewsResult(lastData) } searchPanel == SearchPanel.SpeakersPanel -> item { /* … */ } searchPanel == SearchPanel.TalksPanel -> item { /* … */ } } } } 코드 좀 봅시다

Slide 35

Slide 35 text

‘searchPanel’의 반복이 보임 @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence, id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } val searchPanel = selectedSearchPanel() when { searchPanel is SearchPanel.NewsPanel && !searchPanel.isBlocked -> item { NewsResult(lastData) } searchPanel == SearchPanel.SpeakersPanel -> item { /* … */ } searchPanel == SearchPanel.TalksPanel -> item { /* … */ } } } }

Slide 36

Slide 36 text

{ 서브젝트로 캡쳐 @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence, id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is SearchPanel.NewsPanel && !searchPanel.isBlocked -> item { NewsResult(lastData) } SearchPanel.SpeakersPanel -> item { /* … */ } SearchPanel.TalksPanel -> item { /* … */ } } } } Error: expecting ‘->’

Slide 37

Slide 37 text

{ 가드된 조건: KEEP-371; KT-13626 @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence, id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is SearchPanel.NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SearchPanel.SpeakersPanel -> item { /* … */ } SearchPanel.TalksPanel -> item { /* … */ } } } } 향상된 when: 가드 2.1부터 베타

Slide 38

Slide 38 text

더 나아질 수 있나요? @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence, id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is SearchPanel.NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SearchPanel.SpeakersPanel -> item { /* … */ } SearchPanel.TalksPanel -> item { /* … */ } } } }

Slide 39

Slide 39 text

} } } 문맥 인식 레졸루션 추가 한정어 없음: KT-16768 @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence, id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { Is NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SpeakersPanel -> item { /* … */ } TalksPanel -> item { /* … */ } } } } 2.2에 실험 기능으로 도입

Slide 40

Slide 40 text

enum class Status { Ok, Fail } fun process( status: Status = Status. Ok ) { /* … */ } 문맥 인식 레졸루션

Slide 41

Slide 41 text

문맥 인식 레졸루션 enum class Status { Ok, Fail } fun process( status: Status = Ok ) { /* … */ } 2.2에 실험 기능으로 도입

Slide 42

Slide 42 text

} GADT (일반화된 ADT) 스타일 스마트 캐스트 sealed class Container(val value: T) class IntContainer : Container(42) class StringContainer : Container("Kotlin") fun unbox(container: Container): A = container.value // OK! fun unboxAndProcess(container: Container): A = when (container) { is IntContainer -> container.value // 컴파일 안됨 is StringContainer -> container.value // 컴파일 안됨 }

Slide 43

Slide 43 text

} GADT 스타일 스마트 캐스트 Actual type: String Expected: A sealed class Container(val value: T) class IntContainer : Container(42) class StringContainer : Container("Kotlin") fun unbox(container: Container): A = container.value // OK! fun unboxAndProcess(container: Container): A = when (container) { is IntContainer -> container.value // 컴파일 안됨 is StringContainer -> container.value // 컴파일 안됨 }

Slide 44

Slide 44 text

GADT 스타일 스마트 캐스트 } Actual type: String Expected: A sealed class Container(val value: T) class IntContainer : Container(42) class StringContainer : Container("Kotlin") fun unbox(container: Container): A = container.value // OK! fun unboxAndProcess(container: Container): A = when (container) { is IntContainer -> 42 is StringContainer -> “Kotlin” }

Slide 46

Slide 46 text

이름 기반 비구조화 data class User(val name: String, val lastName: String) fun process(user: User) { val (surname, firstName) = user // … } Error in 2.x: - “surname” doesn’t match the property “name” - “firstName” doesn’t match the property “lastName”

Slide 47

Slide 47 text

이름 기반 비구조화 - 궁극적으로: 더 이상 이름 기반 비구조화에 component 함수를 사용하지 않음 - 새로운 이름으로 대입할 때를 위한 특별한 문법 도입 data class User(val name: String, val lastName: String) fun process(user: User) { val (name, lastName) = usern // OK // … }

Slide 48

Slide 48 text

데이터 인식과 비구조화 - 가드: 바인딩 없는 패턴 매칭 (베타 2.1) - 문맥 인식 레졸루셔션 (베타 2.2) - 일반화된 ADT - 데이터 클래스 향상 - 이름 기반 비구조화 (2.2에서 사전 작업) - 일관된 데이터 클래스 복사 가시성 (2.1에서 사전 작업) - 이펙트 시스템 기능 (여러 릴리스에서 향상)

Slide 49

Slide 49 text

라이브러리는 어떻게 하나요? LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SpeakersPanel -> item { /* … */ } TalksPanel -> item { /* … */ } } }

Slide 50

Slide 50 text

@Composable fun LazyColumn( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, content: LazyListScope.()-> Unit )

Slide 51

Slide 51 text

선택적 매개변수의 API 진화 ● 기본 값을 가지는 새로운 매개변수는 이전 버전과의 호환을 막음 ○ 새로운 오버로드가 추가됨

Slide 52

Slide 52 text

선택적 매개변수의 API 진화 ● 기본 값을 가지는 새로운 매개변수는 이전 버전과의 호환을 막음 ● 매번의 오버로드는 코드 중복과 문서 중복을 야기함.

Slide 53

Slide 53 text

@Composable fun LazyColumn( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalAlignment: Alignment.Horizontal = Alignment.Start, userScrollEnabled: Boolean = true, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), content: LazyListScope.() > Unit )

Slide 54

Slide 54 text

확장 가능한 데이터 인자 dataarg class ColumnSettings( val contentPadding: PaddingValues = PaddingValues(0.dp), val reverseLayout: Boolean = false, val verticalArrangement: Arrangement.Vertical = If (!reverseLayout) Arrangement.Top else Arrangement.Bottom, val horizontalAlignment: Alignment.Horizontal = Alignment.Start, val userScrollEnabled: Boolean = true ) KT-8214 2.2의 예비 변경 사항

Slide 55

Slide 55 text

확장 가능한 데이터 인자 dataarg class ColumnSettings( val contentPadding: PaddingValues = PaddingValues(0.dp), val reverseLayout: Boolean = false, val verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, val horizontalAlignment: Alignment.Horizontal = Alignment.Start, val userScrollEnabled: Boolean = true ) @Composable fun Lazy Column( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), dataarg args: ColumnSettings, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), content: @Composable RowScope.()-> Unit) { // … } LazyColumn(reverseLayout = true) { // … }

Slide 56

Slide 56 text

라이브러리 코드 진화 - 대규모로 진화하는 API: 확장 가능한 데이터 인자 (2.2에서 실험 기능) - 시그니쳐 관리: 강제된 이름 인자 - 반환 값 확인: 반환 값 강제 확인 - KDoc 향상

Slide 57

Slide 57 text

‘Last’ 함수는 어떤가요? LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SpeakersPanel -> item { /* … */ } TalksPanel -> item { /* … */ } } }

Slide 58

Slide 58 text

Right? /** * Returns the last element matching the given [predicate]. */ fun Sequence.last(predicate: (T) -> Boolean): T { var result: T? = null for (element in this) if (predicate(element)) result = element return result ?: throw NoSuchElementException("Not found") }

Slide 59

Slide 59 text

predicate가 { it == null } 이면? fun Sequence.last(predicate: (T) -> Boolean): T { var last: T? = null var found = false for (element in this) { if (predicate(element)) { last = element found = true } } if (!found) throw NoSuchElementException("Not found") @Suppress("UNCHECKED_CAST") return last as T }

Slide 60

Slide 60 text

충분할까요? private object NotFound fun Sequence.last(predicate: (T) -> Boolean): T { var result: Any? = null for (element in this) if (predicate(element)) result = element if (result == NotFound) throw NoSuchElementException("Not found") return result as T }

Slide 61

Slide 61 text

Any?’ 타입의 사용 확인없는 캐스트 private object NotFound fun Sequence.last(predicate: (T) -> Boolean): T { var result: Any? = null for (element in this) if (predicate(element)) result = element if (result == NotFound) throw NoSuchElementException("Not found") return result as T }

Slide 62

Slide 62 text

에러를 위한 유니온 타입 오토 캐스트 에러를 위한 유니온 타입 연구중 private error object NotFound fun Sequence.last(predicate: (T) -> Boolean): T { var result: T | NotFound = NotFound for (element in this) if (predicate(element)) result = element if (result is NotFound) throw NoSuchElementException("Not found") return result }

Slide 63

Slide 63 text

- 일반화된 유니온 타입은 없음, 에러만 가능. - 다른 타입 위치에도 확장 가능 - 특별 연산자도 에러를 지원: ?. !. 연구중 에러를 위한 유니온 타입 private error object NotFound fun Sequence.last(predicate: (T) -> Boolean): T { var result: T | NotFound = NotFound for (element in this) if (predicate(element)) result = element if (result is NotFound) throw NoSuchElementException("Not found") return result }

Slide 64

Slide 64 text

추상화 향상 - Context 파라미터(2.2 베타) - 명시적인 뒷받침 필드 (2.0 실험 기능) - 에러를 위한 유니온 타입 (활발한 연구중) - 스트링 리터럴과 스트링 템플릿(2.1 예비 변경) - 깊은 불변성 (활발한 연구중)

Slide 65

Slide 65 text

명시적 뒷받침 필드 class MyViewModel : ViewModel() { private val _city = MutableLiveData() val city: LiveData get() = _city }

Slide 66

Slide 66 text

명시적 뒷받침 필드 ● 깃헙의 100만개 이상의 파일에서 발견되는 패턴 ● List -> MutableList ● SharedFlow -> MutableSharedFlow ● LiveData -> MutableLiveData ● … class MyViewModel : ViewModel() { private val _city = MutableLiveData() val city: LiveData get() = _city }

Slide 67

Slide 67 text

명시적 뒷받침 필드 class MyViewModel : ViewModel() { private val _city = MutableLiveData() val city: LiveData get() = _city } class MyViewModel : ViewModel() { val city: LiveData field = MutableLiveData() }

Slide 68

Slide 68 text

선택적 이름있는 뒷받침 필드 class MyViewModel : ViewModel() { val city: LiveData field mutableCity = MutableLiveData() } val background: Color field = mutableStateOf(getBackgroundColor) get() = field.value KEEP-278, KT-14663 2.2에 업데이트

Slide 69

Slide 69 text

Share your feedback to help us better understand your KotlinConf’24 Global experience! Subtitle Text

Slide 70

Slide 70 text

Thank you