Jetpack Compose Snapshot System
Jetpack Compose 나만의 State 만들기
Snapshot System 활용
Jetpack Compose 에서 표준으로 사용되는 State 는 4가지가 있습니다.
- SnapshotMutableState (
mutableStateOf()
) - SnapshotStateList (
mutableStateListOf()
) - SnapshotStateMap (
mutableStateMapOf()
) - CompositionLocal (
compositionLocalOf()
)
State 는 리컴포지션을 발생시키기 위한 유일한 표준 방법이며, 우리가 컴포즈를 사용하면서 필수적으로 사용하는 요소입니다. 하지만 이토록 중요한 State 는 위에 나열한 4가지만이 표준이라 우리가 원하는 대로 State 를 사용하기엔 불편함이 있을 수 있습니다.
Pair 를 State 로 설정한 경우를 예로 들어보겠습니다.
Pair 의 모든 필드는 val
이기에 값을 업데이트하기 위해선 매번 copy 로 접근해야 합니다. 저는 pair.first++
와 같이 깔끔하게 접근하길 원합니다.
이를 달성하기 위해 Pair 의 필드를 var
로 가지는 SnapshotMutablePair
를 구현해 보겠습니다. 그전에, 컴포즈에서 State 가 구현되는 원리를 먼저 이해해야 합니다. State 는 Snapshot System 이라는 Compose Runtime 요소로 구현되며, 예전에 자세히 다룬 적이 있습니다.
Snapshot System 의 기본 동작은 “Jetpack Compose 의 스냅샷 시스템” 아티클, Snapshot System 의 동작 원리는 “Jetpack Compose 스냅샷 시스템 분석” 아티클로 확인하실 수 있습니다.
간단하게 복습해 보겠습니다. Snapshot System 은 StateObject 와 StateRecord 로 구성돼 있습니다.
- StateObject: State 를 기록할 객체. StateObject 인스턴스만 State 추적이 가능합니다.
- StateRecord: State 의 값을 나타내는 객체. 이 값이 StateObject 안에 LinkedList 형식으로 기록됩니다.
우리가 var state by mutableStateOf(1)
을 하면 Compose Runtime 에서는 MutableState<Int>
라는 StateObject 가 초기화되고, StateStateRecord(1)
라는 초기 State 값이 state
의 StateObject 에 할당됩니다. (StateStateRecord
는 MutableState
에 기록되는 StateRecord
입니다.)
state = 2
를 하면 StateStateRecord(2)
가 state
의 StateObject 에 기록됩니다. 이 기록되는 과정에서 State 의 값 업데이트를 알리는 콜백이 invoke 되어, 해당 State 를 사용 중인 컴포저블이 모두 리컴포지션됩니다.
여기까지의 Snapshot System 의 기본 컨셉입니다. 자세한 내용은 위 2개의 아티클을 확인해 주세요. 이제 SnapshotMutablePair
구현 준비를 해보겠습니다.
Pair 객체를 나타내는 value
, Pair#first
를 나타내는 first
, Pair#second
를 나타내는 second
로 구성된 인터페이스를 만들어 줍니다.
다음으로 SnapshotMutablePair
에 맞는 StateRecord 를 만들어야 합니다.
StateRecord 는 abstract class 이며 2개의 abstract function 으로 구성돼 있습니다.
create()
: StateRecord 인스턴스 생성 함수assign(value: StateRecord)
: StateRecord 의 값을value
에 맞게 업데이트하는 함수
StateRecord 의 값을 Pair 로 반환하는 asPair()
함수를 추가로 구현하였습니다. 마지막으로, 이렇게 만든 SnapshotPairRecord
를 SnapshotMutablePair
에 연결해 주면 끝입니다.
SnapshotMutablePair
는 State 추적을 당하는 대상입니다. 즉, StateObject 를 구현해야 합니다.
StateObject 는 인터페이스이며 다음과 같은 3가지 필드를 가집니다.
firstStateRecord
: 첫 번째로 기록된 StateRecordprependStateRecord(value: StateRecord)
: 다음 StateRecord 를 기록하는 함수mergeRecords(previous: StateRecord, current: StateRecord, applied: StateRecord): StateRecord?
: 스냅샷 충돌이 발생했을 때 해당 스냅샷들을 병합하는 함수 (스냅샷 충돌이란 git conflict 와 비슷합니다.)
이 글에선 스냅샷 충돌을 고려하지 않습니다.
SnapshotMutablePairImpl
의 필드를 구현해 보겠습니다.
현재 레코드 혹은 다음 레코드를 나타내는 next
변수를 만들어 줍니다. 다음으로 value
, first
, second
를 구현해 주고, firstStateRecord
, prependStateRecord
도 구현해 줍니다.
위 value
, first
, second
구현에서 readale
과 writable
확장 함수가 쓰인 걸 확인할 수 있습니다.
StateRecord#readable
은 해당 StateObject 에 read-access 가 있음을 알리고, 현재 StateObject 가 제공하는 StateRecord 를 조회합니다. StateRecord#writable
은 해당 StateObject 에 write-access 가 있음을 알리고, 값을 저장해야 하는 StateRecord 를 조회합니다.
Compose Runtime 은 read-access 와 write-access 가 모두 보장되는 StateObject 를 사용 중인 컴포저블만 리컴포지션을 활성화하는 최적화를 진행합니다.
이 밖에 StateRecord#withCurrent
확장 함수도 존재합니다. 이 함수는 read-access 알림 없이 해당 StateObject 가 제공하는 StateRecord 를 조회합니다. setter 에서 값 validation 이 필요할 때 사용할 수 있습니다.
SnapshotMutablePair
구현은 이렇게 끝납니다. 이를 깔끔하게 사용하기 위해 mutableStatePairOf
를 선언해 줍니다.
이제 mutableStatePairOf
를 사용해 보겠습니다.
전체 코드: https://gist.github.com/jisungbin/4fb62cecc2f8e09eb7fa8516dfbabc06
끝
State 를 만드는 함수와 클래스들은 모두 public 으로 존재합니다. 이번 글에선 이를 활용하여 나만의 State 만드는 방법을 소개하였습니다.
끝까지 읽어주셔서 감사합니다.
안드로이드 개발자분들을 위한 카카오톡 오픈 채팅방을 운영하고 있습니다.