Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deep Dive into Jetpack Compose State

Deep Dive into Jetpack Compose State

Ji Sungbin

May 22, 2023
Tweet

More Decks by Ji Sungbin

Other Decks in Technology

Transcript

  1. Speaker l
    2023
    찰스의 안드로이드
    컨퍼런스
    Little Deep Dive into

    Jetpack Compose State
    지성빈

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  7. 2023
    찰스의 안드로이드
    컨퍼런스
    이걸 왜 알아야 해?
    •내부 개념인 만큼 컴포즈를 사용한다고 알아야 할 필요는 없다.
    •컴포즈 런타임 구성으로만 쓰이기엔 아까운 기술이고, 대부분 public api로

    구현돼 있어서 컴포즈 내부에 관심이 있다면 한 번쯤 봐볼 가치는 있다.
    •이해하고 있으면 컴포즈의 상태 시스템을 자유자제로 다룰 수 있다.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. class StringPrinter {


    private var value: String? = null


    fun delayedPrint(value: String, delay: Long = 1000L) {


    this.value = value


    Thread.sleep(delay)


    println(this.value)


    }


    }

    View Slide

  12. fun main() {


    val stringPrinter = StringPrinter()


    thread { stringPrinter.delayedPrint("A") }


    thread { stringPrinter.delayedPrint("B") }


    }

    View Slide

  13. fun main() {


    val stringPrinter = StringPrinter()


    thread { stringPrinter.delayedPrint("A") }


    thread { stringPrinter.delayedPrint("B") }


    }


    // result: B, B

    View Slide

  14. 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

    View Slide

  15. 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

    View Slide

  16. 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”

    View Slide

  17. @Composable fun main(state: MutableState) { // 0


    state.value = 1


    state.value = 2


    }
    0 📸
    1
    2

    View Slide

  18. @Composable fun main(state: MutableState) { // 0


    state.value = 1


    state.value = 2


    // recomposition!


    }
    0 📸
    1
    2

    View Slide

  19. @Composable fun main(state: MutableState) { // 0 -> 10


    state.value = 1


    state.value = 2

    // ৻ࠗ੄ ৔ೱਵ۽ state ਗࠄ ч੉


    // زदী ߄Պ


    }
    0 📸
    1
    2
    10

    View Slide

  20. @Composable fun main(state: MutableState) { // 0 -> 10


    state.value = 1


    state.value = 2

    // recomposition!


    }
    0 📸
    1
    2
    10

    View Slide

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

    View Slide

  22. 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

    View Slide

  23. val state = mutableStateOf(0)


    state.value = 1


    state.value = 2


    state.value = 3


    `
    StateObject
    StateRecord

    View Slide

  24. StateObject - StateRecord LinkedList
    // StateRecord LinkedList੄ द੘੼


    val firstStateRecord: StateRecord

    View Slide

  25. StateRecord
    // ׮਺ਵ۽ োѾػ ۨ௏٘


    var next: StateRecord?

    View Slide

  26. StateRecord
    // ࢜۽਍ StateRecord ࢤࢿ


    fun create(): StateRecord

    View Slide

  27. StateRecord
    // ઱য૓ StateRecordীࢲ ч ࠂࢎ


    fun assign(value: StateRecord)

    View Slide

  28. StateRecord
    // mutableೠ о੢ ୭न ۨ௏٘ী ੘স ࣻ೯


    fun writable(


    state: StateObject,


    block: StateRecord.() -> R,


    ): R

    View Slide

  29. StateRecord
    // о੢ ୭न ۨ௏٘ী ੘স ࣻ೯


    fun withCurrent(


    block: (record: StateRecord) -> R,


    ): R

    View Slide

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

    View Slide

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

    View Slide

  32. // അ੤ ೐ۨ੐ ੋؙझ


    var currentFrame = 0


    // пп ೐ۨ੐߹ ࢚క ч(StateRecord)


    val frames = mutableListOf()

    View Slide

  33. // ч(StateRecord) ߸҃ਸ ୶੸ೡ ࢚క(StateObject)


    var target: StateObject? = null

    View Slide

  34. // ୶੸ ઺ੋ ࢚క੄ ч(StateRecord)ਸ ೐ۨ੐ ߓৌী ੷੢


    fun saveFrame() {


    frames += target!!.copyCurrentRecord()


    currentFrame++


    }

    View Slide

  35. // ઱য૓ ࢚కী ೡ׼ػ ч(StateRecord)ਸ ࠂࢎೞৈ ߈ജ


    fun StateObject.copyCurrentRecord(): StateRecord {


    val newRecord = firstStateRecord.create()


    firstStateRecord.withCurrent { current ->


    newRecord.assign(current)


    }


    return newRecord


    }

    View Slide

  36. // ઱য૓ State੄ ࢚క ߸҃ਸ ୶੸ೞب۾ ૑੿


    fun MutableState.track(): MutableState {


    target = this as StateObject


    return this


    }


    View Slide

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

    View Slide

  38. // ୶੸ ઺ੋ ࢚క੄ чਸ ੉੹ ೐ۨ੐ਵ۽ ࠂਗ


    fun undo() {


    if (currentFrame - 1 in frames.indices) {


    target!!.restoreFrom(frames[--currentFrame])


    }


    }


    View Slide

  39. // ࢚క੄ чਸ ઱য૓ ч(StateRecord)ਵ۽ ૑੿


    fun StateObject.restoreFrom(record: StateRecord) {


    firstStateRecord.writable(this) {


    assign(record)


    }


    }


    View Slide

  40. // ୶੸ ઺ੋ ࢚క੄ чਸ ׮਺ ೐ۨ੐ਵ۽ ࠂਗ


    fun redo() {


    if (currentFrame + 1 in frames.indices) {


    target!!.restoreFrom(frames[++currentFrame])


    }


    }


    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  45. // StateObjectী StateRecordо ੘ࢿؼ ٸ ഐ୹غח ௒ߔ ١۾


    // Set: ੘ࢿػ StateObject ݽ਺


    context(Snapshot) fun registerApplyObserver(


    observer: (Set, Snapshot) -> Unit,


    ): ObserverHandle

    View Slide

  46. // registerApplyObserver۽ ١۾ೠ ௒ߔਸ ೧ઁೞח ೩ٜ


    fun interface ObserverHandle {


    fun dispose()


    }

    View Slide

  47. // ࢚క ч(StateRecord) ߸҃ ୶੸ਸ ஂࣗೞח ೩ٜ


    var handle: ObserverHandle? = null

    View Slide

  48. // ୶੸ ઺ੋ ࢚కо ߸ೡ ٸ݃׮ saveFrame() ഐ୹ द੘


    fun startRecording() {


    handle = Snapshot.registerApplyObserver { states, _ ->


    if (states.any { it == target }) {


    saveFrame()


    }


    }


    }

    View Slide

  49. // ୶੸ ઺ੋ ࢚కо ߸ೡ ٸ݃׮ saveFrame() ഐ୹ ઺૑


    fun stopRecording() {


    handle!!.dispose()


    }

    View Slide

  50. // ઱য૓ State੄ ࢚క ߸҃ਸ ୶੸ೞب۾ ૑੿


    fun MutableState.track(): MutableState {


    target = this as StateObject


    startRecording()


    return this


    }


    View Slide

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

    View Slide

  52. // ୶੸ ઺ੋ ࢚క੄ чਸ ੉੹ ೐ۨ੐ਵ۽ ࠂਗ


    fun undo() {


    stopRecording()


    if (currentFrame - 1 in frames.indices) {


    target!!.restoreFrom(frames[--currentFrame])


    }


    startRecording()


    }


    View Slide

  53. // ୶੸ ઺ੋ ࢚క੄ чਸ ׮਺ ೐ۨ੐ਵ۽ ࠂਗ


    fun redo() {


    stopRecording()


    if (currentFrame + 1 in frames.indices) {


    target!!.restoreFrom(frames[++currentFrame])


    }


    startRecording()


    }


    View Slide

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

    View Slide

  55. 2023
    찰스의 안드로이드
    컨퍼런스
    프레임 처리 정책
    과거 프레임에서 신규 프레임을 저장하면

    과거와 현재 사이의 모든 프레임 제거 후 신규 프레임 저장

    View Slide

  56. // ୶੸ ઺ੋ ࢚క੄ ч(StateRecord)ਸ ೐ۨ੐ ݾ۾ী ੷੢


    fun saveFrame() {


    if (currentFrame < frames.lastIndex) {


    frames.removeRange(start = currentFrame + 1,

    end = frames.size)


    }


    frames += target!!.copyCurrentRecord()


    currentFrame++


    }

    View Slide

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

    View Slide

  58. var currentFrame = 0


    val frames = mutableListOf()


    var target: StateObject? = null


    var handle: ObserverHandle? = null

    View Slide

  59. fun StateObject.copyCurrentRecord(): StateRecord {


    val newRecord = firstStateRecord.create()


    firstStateRecord.withCurrent { current ->


    newRecord.assign(current)


    }


    return newRecord


    }

    View Slide

  60. fun saveFrame() {


    if (currentFrame < frames.lastIndex) {


    frames.removeRange(currentFrame + 1, frames.size)


    }


    frames += target!!.copyCurrentRecord()


    currentFrame++


    }

    View Slide

  61. fun startRecording() {


    handle = Snapshot.registerApplyObserver { states, _ ->


    if (states.any { it == target }) {


    saveFrame()


    }


    }


    }

    View Slide

  62. fun stopRecording() {


    handle!!.dispose()


    }


    View Slide

  63. fun MutableState.track(): MutableState {


    target = this as StateObject


    startRecording()


    return this


    }


    View Slide

  64. fun StateObject.restoreFrom(record: StateRecord) {


    firstStateRecord.writable(this) {


    assign(record)


    }


    }


    View Slide

  65. fun undo() {


    stopRecording()


    if (currentFrame - 1 in frames.indices) {


    target!!.restoreFrom(frames[--currentFrame])


    }


    startRecording()


    }

    View Slide

  66. fun redo() {


    stopRecording()


    if (currentFrame + 1 in frames.indices) {


    target!!.restoreFrom(frames[++currentFrame])


    }


    startRecording()


    }


    View Slide

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

    View Slide

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

    View Slide

  69. // 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,


    )

    View Slide

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

    View Slide