Slide 1

Slide 1 text

Speaker l 2023 찰스의 안드로이드 컨퍼런스 Little Deep Dive into 
 Jetpack Compose State 지성빈

Slide 2

Slide 2 text

Speaker l 2023 찰스의 안드로이드 컨퍼런스 •성빈랜드 안드로이드 기술 블로그 운영 •보충역 병역특례 취업 준비 중 지성빈

Slide 3

Slide 3 text

2023 찰스의 안드로이드 컨퍼런스 목표 •스냅샷 시스템의 컨셉을 이해한다. •StateObject와 StateRecord를 이해한다. •Undo/Redo 시스템을 만들 수 있다. •발표의 이해도를 높이기 위해 목표와 무관한 내용은 전체 생략

Slide 4

Slide 4 text

2023 찰스의 안드로이드 컨퍼런스 Compose Artifacts 🎨 Ui 🛠 Runtime

Slide 5

Slide 5 text

2023 찰스의 안드로이드 컨퍼런스 Compose Artifacts 🍀 Node 📸 Snapshot

Slide 6

Slide 6 text

2023 찰스의 안드로이드 컨퍼런스 📸 Snapshot System •컴포즈에서 두 번째로 깊은 계층 •State의 구현체 •리컴포지션의 뼈대

Slide 7

Slide 7 text

2023 찰스의 안드로이드 컨퍼런스 이걸 왜 알아야 해? •내부 개념인 만큼 컴포즈를 사용한다고 알아야 할 필요는 없다. •컴포즈 런타임 구성으로만 쓰이기엔 아까운 기술이고, 대부분 public api로 
 구현돼 있어서 컴포즈 내부에 관심이 있다면 한 번쯤 봐볼 가치는 있다. •이해하고 있으면 컴포즈의 상태 시스템을 자유자제로 다룰 수 있다.

Slide 8

Slide 8 text

2023 찰스의 안드로이드 컨퍼런스 1. What is Snapshot System?

Slide 9

Slide 9 text

2023 찰스의 안드로이드 컨퍼런스 📸 Snapshot System 컨셉 컴포지션은 특정 순서에 구애받지 않고 무작위 병렬로 실행된다. → 하나의 State에 여러 컴포지션이 동시에 접근할 수 있으며, 동시성 문제에 빠질 수 있음

Slide 10

Slide 10 text

2023 찰스의 안드로이드 컨퍼런스 📸 Snapshot System 컨셉 💡 모든 State 연산을 고립되게 진행하자! 컴포지션은 특정 순서에 구애받지 않고 무작위 병렬로 실행된다. → 하나의 State에 여러 컴포지션이 동시에 접근할 수 있으며, 동시성 문제에 빠질 수 있음

Slide 11

Slide 11 text

class StringPrinter { private var value: String? = null fun delayedPrint(value: String, delay: Long = 1000L) { this.value = value Thread.sleep(delay) println(this.value) } }

Slide 12

Slide 12 text

fun main() { val stringPrinter = StringPrinter() thread { stringPrinter.delayedPrint("A") } thread { stringPrinter.delayedPrint("B") } }

Slide 13

Slide 13 text

fun main() { val stringPrinter = StringPrinter() thread { stringPrinter.delayedPrint("A") } thread { stringPrinter.delayedPrint("B") } } // result: B, B

Slide 14

Slide 14 text

class StringPrinter { private val value = ThreadLocal() fun delayedPrint(value: String, delay: Long = 1000L) { this.value.set(value) Thread.sleep(delay) println(this.value.get()) } } // result: A, B

Slide 15

Slide 15 text

class StringPrinter { private val value = ThreadLocal() fun delayedPrint(value: String, delay: Long = 1000L) { this.value.set(value) Thread.sleep(delay) println(this.value.get()) } } // result: A, B Snapshot

Slide 16

Slide 16 text

class StringPrinter { private val value = ThreadLocal() fun delayedPrint(value: String, delay: Long = 1000L) { this.value.set(value) Thread.sleep(delay) println(this.value.get()) } } // result: A, B Snapshot “Snapshot System”

Slide 17

Slide 17 text

@Composable fun main(state: MutableState) { // 0 state.value = 1 state.value = 2 } 0 📸 1 2

Slide 18

Slide 18 text

@Composable fun main(state: MutableState) { // 0 state.value = 1 state.value = 2 // recomposition! } 0 📸 1 2

Slide 19

Slide 19 text

@Composable fun main(state: MutableState) { // 0 -> 10 state.value = 1 state.value = 2 
 // ৻ࠗ੄ ৔ೱਵ۽ state ਗࠄ ч੉ // زदী ߄Պ } 0 📸 1 2 10

Slide 20

Slide 20 text

@Composable fun main(state: MutableState) { // 0 -> 10 state.value = 1 state.value = 2 
 // recomposition! } 0 📸 1 2 10

Slide 21

Slide 21 text

2023 찰스의 안드로이드 컨퍼런스 2. Snapshot System API

Slide 22

Slide 22 text

class StringPrinter { private val value = ThreadLocal() fun delayedPrint(value: String, delay: Long = 1000L) { this.value.set(value) Thread.sleep(delay) println(this.value.get()) } } ` StateObject StateRecord

Slide 23

Slide 23 text

val state = mutableStateOf(0) state.value = 1 state.value = 2 state.value = 3 ` StateObject StateRecord

Slide 24

Slide 24 text

StateObject - StateRecord LinkedList // StateRecord LinkedList੄ द੘੼ val firstStateRecord: StateRecord

Slide 25

Slide 25 text

StateRecord // ׮਺ਵ۽ োѾػ ۨ௏٘ var next: StateRecord?

Slide 26

Slide 26 text

StateRecord // ࢜۽਍ StateRecord ࢤࢿ fun create(): StateRecord

Slide 27

Slide 27 text

StateRecord // ઱য૓ StateRecordীࢲ ч ࠂࢎ fun assign(value: StateRecord)

Slide 28

Slide 28 text

StateRecord // mutableೠ о੢ ୭न ۨ௏٘ী ੘স ࣻ೯ fun writable( state: StateObject, block: StateRecord.() -> R, ): R

Slide 29

Slide 29 text

StateRecord // о੢ ୭न ۨ௏٘ী ੘স ࣻ೯ fun withCurrent( block: (record: StateRecord) -> R, ): R

Slide 30

Slide 30 text

2023 찰스의 안드로이드 컨퍼런스 3. Undo/Redo System

Slide 31

Slide 31 text

2023 찰스의 안드로이드 컨퍼런스 Undo/Redo 시스템 1. 상태가 변경될 때마다 새로운 상태 값 저장 2. Undo 요청 시 이전 값으로 상태 복원 3. Redo 요청 시 다음 값으로 상태 복원

Slide 32

Slide 32 text

// അ੤ ೐ۨ੐ ੋؙझ var currentFrame = 0 // пп ೐ۨ੐߹ ࢚క ч(StateRecord) val frames = mutableListOf()

Slide 33

Slide 33 text

// ч(StateRecord) ߸҃ਸ ୶੸ೡ ࢚క(StateObject) var target: StateObject? = null

Slide 34

Slide 34 text

// ୶੸ ઺ੋ ࢚క੄ ч(StateRecord)ਸ ೐ۨ੐ ߓৌী ੷੢ fun saveFrame() { frames += target!!.copyCurrentRecord() currentFrame++ }

Slide 35

Slide 35 text

// ઱য૓ ࢚కী ೡ׼ػ ч(StateRecord)ਸ ࠂࢎೞৈ ߈ജ fun StateObject.copyCurrentRecord(): StateRecord { val newRecord = firstStateRecord.create() firstStateRecord.withCurrent { current -> newRecord.assign(current) } return newRecord }

Slide 36

Slide 36 text

// ઱য૓ State੄ ࢚క ߸҃ਸ ୶੸ೞب۾ ૑੿ fun MutableState.track(): MutableState { target = this as StateObject return this }

Slide 37

Slide 37 text

2023 찰스의 안드로이드 컨퍼런스 Undo/Redo 시스템 1. 상태가 변경될 때마다 새로운 상태 값 저장 2. Undo 요청 시 이전 값으로 상태 복원 3. Redo 요청 시 다음 값으로 상태 복원

Slide 38

Slide 38 text

// ୶੸ ઺ੋ ࢚క੄ чਸ ੉੹ ೐ۨ੐ਵ۽ ࠂਗ fun undo() { if (currentFrame - 1 in frames.indices) { target!!.restoreFrom(frames[--currentFrame]) } }

Slide 39

Slide 39 text

// ࢚క੄ чਸ ઱য૓ ч(StateRecord)ਵ۽ ૑੿ fun StateObject.restoreFrom(record: StateRecord) { firstStateRecord.writable(this) { assign(record) } }

Slide 40

Slide 40 text

// ୶੸ ઺ੋ ࢚క੄ чਸ ׮਺ ೐ۨ੐ਵ۽ ࠂਗ fun redo() { if (currentFrame + 1 in frames.indices) { target!!.restoreFrom(frames[++currentFrame]) } }

Slide 41

Slide 41 text

2023 찰스의 안드로이드 컨퍼런스 Undo/Redo 시스템 1. 상태가 변경될 때마다 새로운 상태 값 저장 2. Undo 요청 시 이전 값으로 상태 복원 3. Redo 요청 시 다음 값으로 상태 복원

Slide 42

Slide 42 text

2023 찰스의 안드로이드 컨퍼런스 지금까지 만든 Undo/Redo 시스템 1. saveFrame(): 현재 프레임의 상태 값(StateRecord) 저장 2. track(): 추적할 상태(StateObject) 지정 3. undo(): 이전 프레임으로 상태 값(StateRecord) 복원 4. redo(): 다음 프레임으로 상태 값(StateRecord) 복원

Slide 43

Slide 43 text

2023 찰스의 안드로이드 컨퍼런스 이제 만들 Undo/Redo 시스템 1. saveFrame() 호출 시점 2. 과거 프레임에서 신규 프레임을 저장했을 때 프레임 처리 정책

Slide 44

Slide 44 text

2023 찰스의 안드로이드 컨퍼런스 saveFrame() 호출 시점 • 추적 중인 상태(StateObject)에 값(StateRecord)이 작성됐을 때 🙆 • undo()와 redo()로 상태 값(StateRecord)이 복원됐을 때 🙅

Slide 45

Slide 45 text

// StateObjectী StateRecordо ੘ࢿؼ ٸ ഐ୹غח ௒ߔ ١۾ // Set: ੘ࢿػ StateObject ݽ਺ context(Snapshot) fun registerApplyObserver( observer: (Set, Snapshot) -> Unit, ): ObserverHandle

Slide 46

Slide 46 text

// registerApplyObserver۽ ١۾ೠ ௒ߔਸ ೧ઁೞח ೩ٜ fun interface ObserverHandle { fun dispose() }

Slide 47

Slide 47 text

// ࢚క ч(StateRecord) ߸҃ ୶੸ਸ ஂࣗೞח ೩ٜ var handle: ObserverHandle? = null

Slide 48

Slide 48 text

// ୶੸ ઺ੋ ࢚కо ߸ೡ ٸ݃׮ saveFrame() ഐ୹ द੘ fun startRecording() { handle = Snapshot.registerApplyObserver { states, _ -> if (states.any { it == target }) { saveFrame() } } }

Slide 49

Slide 49 text

// ୶੸ ઺ੋ ࢚కо ߸ೡ ٸ݃׮ saveFrame() ഐ୹ ઺૑ fun stopRecording() { handle!!.dispose() }

Slide 50

Slide 50 text

// ઱য૓ State੄ ࢚క ߸҃ਸ ୶੸ೞب۾ ૑੿ fun MutableState.track(): MutableState { target = this as StateObject startRecording() return this }

Slide 51

Slide 51 text

2023 찰스의 안드로이드 컨퍼런스 saveFrame() 호출 시점 • 추적 중인 상태(StateObject)에 값(StateRecord)이 작성됐을 때 🙆 • undo()와 redo()로 상태 값(StateRecord)이 복원됐을 때 🙅

Slide 52

Slide 52 text

// ୶੸ ઺ੋ ࢚క੄ чਸ ੉੹ ೐ۨ੐ਵ۽ ࠂਗ fun undo() { stopRecording() if (currentFrame - 1 in frames.indices) { target!!.restoreFrom(frames[--currentFrame]) } startRecording() }

Slide 53

Slide 53 text

// ୶੸ ઺ੋ ࢚క੄ чਸ ׮਺ ೐ۨ੐ਵ۽ ࠂਗ fun redo() { stopRecording() if (currentFrame + 1 in frames.indices) { target!!.restoreFrom(frames[++currentFrame]) } startRecording() }

Slide 54

Slide 54 text

2023 찰스의 안드로이드 컨퍼런스 이제 만들 Undo/Redo 시스템 1. saveFrame() 호출 시점 2. 과거 프레임에서 신규 프레임을 저장했을 때 프레임 처리 정책

Slide 55

Slide 55 text

2023 찰스의 안드로이드 컨퍼런스 프레임 처리 정책 과거 프레임에서 신규 프레임을 저장하면 
 과거와 현재 사이의 모든 프레임 제거 후 신규 프레임 저장

Slide 56

Slide 56 text

// ୶੸ ઺ੋ ࢚క੄ ч(StateRecord)ਸ ೐ۨ੐ ݾ۾ী ੷੢ fun saveFrame() { if (currentFrame < frames.lastIndex) { frames.removeRange(start = currentFrame + 1, 
 end = frames.size) } frames += target!!.copyCurrentRecord() currentFrame++ }

Slide 57

Slide 57 text

2023 찰스의 안드로이드 컨퍼런스 Undo/Redo System 완성! ✌ (전체 코드는 60줄)

Slide 58

Slide 58 text

var currentFrame = 0 val frames = mutableListOf() var target: StateObject? = null var handle: ObserverHandle? = null

Slide 59

Slide 59 text

fun StateObject.copyCurrentRecord(): StateRecord { val newRecord = firstStateRecord.create() firstStateRecord.withCurrent { current -> newRecord.assign(current) } return newRecord }

Slide 60

Slide 60 text

fun saveFrame() { if (currentFrame < frames.lastIndex) { frames.removeRange(currentFrame + 1, frames.size) } frames += target!!.copyCurrentRecord() currentFrame++ }

Slide 61

Slide 61 text

fun startRecording() { handle = Snapshot.registerApplyObserver { states, _ -> if (states.any { it == target }) { saveFrame() } } }

Slide 62

Slide 62 text

fun stopRecording() { handle!!.dispose() }

Slide 63

Slide 63 text

fun MutableState.track(): MutableState { target = this as StateObject startRecording() return this }

Slide 64

Slide 64 text

fun StateObject.restoreFrom(record: StateRecord) { firstStateRecord.writable(this) { assign(record) } }

Slide 65

Slide 65 text

fun undo() { stopRecording() if (currentFrame - 1 in frames.indices) { target!!.restoreFrom(frames[--currentFrame]) } startRecording() }

Slide 66

Slide 66 text

fun redo() { stopRecording() if (currentFrame + 1 in frames.indices) { target!!.restoreFrom(frames[++currentFrame]) } startRecording() }

Slide 67

Slide 67 text

2023 찰스의 안드로이드 컨퍼런스

Slide 68

Slide 68 text

2023 찰스의 안드로이드 컨퍼런스

Slide 69

Slide 69 text

// duckie-team/quack-quack-android Text( modifier = Modifier.span( texts = listOf("QuackQuack"), style = SpanStyle(color = Orange), ), text = "QuackQuack is an awesome ui kit.", typography = Body1, )

Slide 70

Slide 70 text

2023 찰스의 안드로이드 컨퍼런스 감사합니다. jisungbin/SimpleStateHistory