Jetpack Compose Snapshot System

Jetpack Compose 나만의 State 만들기

Snapshot System 활용

Ji Sungbin
성빈랜드

--

Photo by Kenny Eliason on Unsplash

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 에 할당됩니다. (StateStateRecordMutableState 에 기록되는 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() 함수를 추가로 구현하였습니다. 마지막으로, 이렇게 만든 SnapshotPairRecordSnapshotMutablePair 에 연결해 주면 끝입니다.

SnapshotMutablePair 는 State 추적을 당하는 대상입니다. 즉, StateObject 를 구현해야 합니다.

StateObject 는 인터페이스이며 다음과 같은 3가지 필드를 가집니다.

  • firstStateRecord: 첫 번째로 기록된 StateRecord
  • prependStateRecord(value: StateRecord): 다음 StateRecord 를 기록하는 함수
  • mergeRecords(previous: StateRecord, current: StateRecord, applied: StateRecord): StateRecord?: 스냅샷 충돌이 발생했을 때 해당 스냅샷들을 병합하는 함수 (스냅샷 충돌이란 git conflict 와 비슷합니다.)

이 글에선 스냅샷 충돌을 고려하지 않습니다.

SnapshotMutablePairImpl 의 필드를 구현해 보겠습니다.

현재 레코드 혹은 다음 레코드를 나타내는 next 변수를 만들어 줍니다. 다음으로 value, first, second 를 구현해 주고, firstStateRecord, prependStateRecord 도 구현해 줍니다.

value, first, second 구현에서 readalewritable 확장 함수가 쓰인 걸 확인할 수 있습니다.

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 만드는 방법을 소개하였습니다.

끝까지 읽어주셔서 감사합니다.

안드로이드 개발자분들을 위한 카카오톡 오픈 채팅방을 운영하고 있습니다.

--

--

Ji Sungbin
성빈랜드

Experience Engineers for us. I love development that creates references.