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

Jetpack Compose: How it works?

Jetpack Compose: How it works?

!! 주의 !!

글쓰기 공부를 하기 전에 작성했던 대본을 재사용하여, 지저분하고 맞춤법이 틀린 부분이 꽤 있습니다. 이 점은 양해 부탁드립니다.

### 타이틀

안녕하세요, 저는 Jetpack Compose, How it works? 라는 주제로 발표할 지성빈입니다. 아직 취업 준비중이라 소개할게 없어서 제 소개는 건너뛰도록 하겠습니다.

### 목차

이번 발표는 컴포즈와 더 친해지는 시간으로 간단하게 컴포즈의 내부 구현을 소개하려 합니다. 컴포즈는 컴파일러와 런타임 영역으로 나뉩니다.

그 전에! 컴포즈를 제대로 이해하기 위해선 몇몇 기본 개념들이 필요합니다. 해당 개념들을 설명하는 것으로 발표를 시작하겠습니다.

### @Composable

모든 컴포즈 함수들은 Composable 어노테이션이 붙습니다.

### @Comoposable - 2

이 어노테이션은 멱등성의 보장, 위치 메모이제이션 활성화, 데이터 제공 또는 컴포저블 방출. 이렇게 3가지의 역할을 갖습니다.

이 어노테이션이 붙은 함수를 컴포저블 이라고 부르며, 각각 역할이 갖는 의미가 아주 중요함으로 차례대로 알아보겠습니다.

### 멱등성

우선 모든 컴포저블은 멱등성을 보장해야 합니다. 모든 컴포즈 시스템은 컴포저블이 멱등성을 보장한다는 가정 하에 설계됐습니다. 멱등성이 보장됨으로써 하나의 컴포저블이 여러번 실행되더라도 input 이 동일하다면 output 또한 동일할 것이기 때문에 재실행을 건너뛰게 됩니다.

### 멱등성 - 2

또한 컴포저블은 실행되는 순서가 정해져 있지 않습니다. 예를 들어 화면의 일부분만 차지하는 A 컴포저블이 있고, 화면의 전체를 차지하는 B 컴포저블이 있습니다. 이런 경우는 A 다음에 B 를 실행한다 해도 B 컴포저블이 실행되면 A 컴포저블은 B 에 가려져서 보이지 않기 때문에 B 컴포저블이 먼저 실행됩니다. 즉, 컴포저블은 런타임에서 결정된 우선 순위에 맞게 병렬로 실행됨으로 컴포저블의 실행 순서에 의존하는 건 옳지 않습니다. 이런 이유로도 멱등성을 보장하는 것이 중요합니다.

### 위치 메모이제이션

다음으로 위치 메모이제이션 입니다. 모든 컴포저블은 함수의 시그니처와 call-site, 그리고 실행된 순서를 기준으로 고유한 key 가 생성되고, 컴포즈 런타임에서는 해당 key 를 기준으로 해당 컴포저블의 모든 데이터를 저장하고 관리합니다.

이후 같은 key 를 가진 컴포저블이 같은 input 으로 다시 실행된다면 재실행하는 대신 기존에 실행된 결과를 그대로 재사용합니다. 이는 명백한 멱등성과 위치 메모이제이션의 결과 입니다.

### @Composable 의 역할 - 데이터 제공

이런 컴포저블은 2가지의 종류로 나뉩니다. stringResource 함수처럼 데이터를 제공하거나,

### @Composable 의 역할 - 방출

ByeWorld 함수처럼 컴포저블을 방출할 수 있습니다.

ByeWorld 함수의 컴포저블 역할인 컴포저블 방출에서, 방출은 무엇을 의미하는 것일까요?

### SlotTable

방출을 이해하기 위해선 컴포즈 런타임에서 모든 데이터가 저장되고 관리되는 SlotTable 에 대해 이해해야 합니다. SlotTable 은 갭 버퍼와 랜덤 액새스를 이용하여 Any를 담는 배열에 모든 데이터를 저장하고 관리하는 클래스 입니다.

### Gap Buffer

많은 분들이 갭 버퍼에 대해 생소하실 거 같아서 간단하게 Gap Buffer 를 소개하겠습니다. 텍스트 에디터를 생각해 봅시다. 입력되는 텍스트가 일반 선형 배열에 저장되고, 만약 텍스트가 중간에 삽입되거나 삭제된다면 뒤따라오는 나머지 텍스트들의 인덱스를 조정하느라 O(N) 이 걸립니다. 하지만 갭 버퍼를 이용하여 텍스트를 저장한다면 O(1) 만에 가능합니다.

갭 버퍼는 사용할 공간들을 미리 확보해 두고, 해당 공간에서 작업을 진행하는 자료 구조 입니다. 위키피디아 에서는 동일한 위치 근처에 클러스터링된 효율적인 삽입 및 삭제 작업을 허용하는 동적 배열 이라고 정의하고 있습니다.

### Gap Buffer - 2

갭 버퍼의 작동 원리에 대해 보겠습니다. 먼저 사용할 공간들을 확보하는 작업이 필요합니다. 이 작업은 인덱스를 확장하느라 O(N) 이 걸리고, 이렇게 확보된 공간을 Gap 이라고 부릅니다.

### Gap Buffer - 3

Hi 글자를 삽입해 보겠습니다. 이 작업은 모두 Gap 에서 진행됨으로 O(1) 에 완료됩니다.

### Gap Buffer - 4

첫 번째 인덱스에 있는 i 를 제거해 보겠습니다. 커서를 i 가 있는 위치로 옮기기 위해 O(N) 이 걸리고, 이후 갭에서 값 제거가 진행되므로 O(1) 에 완료됩니다.

### Gap Buffer - 5

마지막으로 H 를 Bye 로 변경해 보겠습니다. 커서를 H 가 있는 위치로 옮기기 위해 O(N) 이 걸리고, 이후 모두 갭에서 값 업데이트가 진헹되므로 O(1) 에 완료됩니다.

### SlotTable

이 개념을 그대로 SlotTable 로 대입해 보겠습니다. isLoading 값에 따라 텍스트를 동적으로 추가시켜 보여주는 컴포저블이 있습니다. 현재 isLoading 값은 false 이므로 Column 과 Column Data 텍스트 까지만 슬릇 테이블에 올라갑니다. 이 과정은 초기 갭을 할당하는 과정만 O(N) 이 걸리고 이후 값 업데이트는 O(1) 에 진행됩니다.

### SlotTable - 2

isLoading 값이 true 로 바뀌어 Loading 텍스트가 보여져야 한다면 커서를 Loading 텍스트가 들어갈 위치로 옮기느라 O(N) 이 걸리고 이후 값 업데이트는 O(1) 에 완료됩니다.

### Anchor

이렇게 매번 작업마다 커서를 이동하느라 O(N) 이 걸리는건 매우 비효율 적입니다. 따라서 슬릇 테이블은 Anchor 라는 개념을 도입하여 랜덤 액세스를 활성화 시킵니다. Anchor 란 갭을 무시하고 특정 위치의 절대적인 인덱스를 가르키는 래퍼입니다. 이 Anchor 를 이용하여 랜덤 액세스를 활성화 시켜 커서를 조정하는 O(N) 시간을 없애게 됩니다.

### Random Access

따라서 아까와 같이 Loading 텍스트가 동적으로 추가되는 상황에서 Anchor 를 이용함으로써 O(1) 만에 값 업데이트가 진행됩니다.

### Gap Buffer 사용 이유

다른 예제를 보겠습니다. isColumn 값에 따라 루트 레이아웃을 동적으로 변경시켜 보여주는 컴포저블이 있습니다. 초기엔 isColumn 값이 true 이므로 Column 을 기준으로 슬릇 테이블의 갭이 형성됩니다.

### Gap Buffer 사용 이유 - 2

isColumn 값이 false 로 변경돼 루트 레이아웃이 Row 로 변경된 경우에는 루트 레이아웃이 변경됨으로 하위 아이템들의 개수가 기존과 동일할 것이라고 확신하지 못해 새로운 루트인 Row 를 기준으로 갭을 처음부터 다시 할당합니다. 즉, 이 시점에서는 O(N) 이 소요됩니다.

하지만 실제 UI 에서는 이렇게 루트 레이아웃이 동적으로 변경되는 상황이 많이 없으므로 초기 갭 할당 이후로는 모든 작업이 O(1) 로 진행될 가능성이 높습니다. 따라서 컴포즈 개발팀에서는 슬릇 테이블의 데이터 저장 방식으로 Gap Buffer 를 채택하였습니다.

### Composition

이제 컴포즈 용어들에 대해 소개하겠습니다. 슬릇 테이블에 초기 갭이 할당되고 최초 상태의 컴포저블 데이터가 등록되는 것을 컴포지션 이라고 부릅니다.

### Recomposition

이후 상태 변경으로 인해 컴포저블이 변동되어 슬릇 테이블에 값 업데이트가 진행되는 것을 리컴포지션 이라고 합니다. 이 때, 컴포저블의 멱등성의 보장으로 상태 변경의 영향을 받은 컴포저블 의외의 다른 데이터들은 업데이트를 하지 않고 기존에 있던 값을 그대로 사용합니다. 이렇게 상태 변경에 영향을 받은 부분만 슬릇 테이블 업데이트를 진행하는 것을 스마트-리컴포지션 이라고 합니다.

### Emit

다음으로 컴포지션으로 구성된 슬릇 테이블을 읽고 UI 트리를 구축하는 과정을 방출 이라고 합니다.

### **Materializing**

마지막으로 방출된 트리를 읽고 실제로 UI 로 그리는 과정을 구체화 라고 합니다. 구체화 과정은 컴포즈에서 재설계한 안드로이드의 컨버스를 이용해서 진행됩니다. 이렇게 3개의 과정을 걸쳐서 컴포저블이 실제로 그려지게 됩니다.

### Snapshot System

여기까지의 소개에서 한 가지 빠진 점이 있습니다. 컴포즈에서는 상태의 변경을 어떻게 감지하고, 변경에 맞게 리컴포지션을 진행하는 걸까요?

### Snapshot System - 2

모든 상태는 Snapshot 이라는 시스템에서 관리됩니다. 스냅샷 시스템은 컴포즈에서 추적할 수 있는 상태를 만드는데 사용되고, mutableStateOf 를 이용하여 변경 가능한 상태를 만들 수 있습니다. 스냅샷 시스템은 내부에서 MVCC 라는 개념을 이용하여 동시성을 고려하여 설계됐습니다. 따라서 thread-safe 하게 상태에 접근할 수 있습니다. MVCC 를 다루기엔 발표가 너무 어려워지므로 설명은 생략하도록 하겠습니다.

### How it works? - Compiler

지금까지 컴포즈의 핵심 개념들에 대해 보았습니다. 이런 컴포즈는 내부에서 어떤 마법으로 작동될까요? 컴파일러부터 보겠습니다.

### Without Annotation Processer

모든 컴포즈 함수들은 Composable 어노테이션이 붙는다고 했습니다. 하지만 Composable 어노테이션을 처리하기 위한 어노테이션 프로세서가 존재하지 않습니다. 이런 환경에서 어떻게 컴포저블이 작동할까요?

### Compose Kotlin Compiler

정답은 컴포즈 코틀린 컴파일러에 있습니다. 컴포즈를 사용하기 위해선 buildFeatures 에서 compose 를 true 로 해야 합니다. 또는 코틀린 컴파일 플러그인에 컴포즈 컴파일러를 바로 등록시켜주는 방법도 있습니다. buildFeatures 를 통한 설정도 내부는 이 방법으로 작동합니다.

컴포즈는 어노테이션 프로세서가 아닌 코틀린 컴파일러로 작동함으로써 코틀린 언어의 컴파일 과정으로 임배드하여 처리 속도를 높이고, 어노테이션 프로세서는 할 수 없는 높은 수준으로 코드 접근을 하여 컴파일됩니다.

예를 들어, 코틀린과 컴포즈는 멀티플랫폼을 대상으로 하므로 컴파일 과정에서 생성되는 IR 을 현재 플랫폼에 맞는 환경으로 수정하고 lowering(로어링) 을 진행합니다.

### Compile steps - full

컴포즈 컴파일러는 크게 12단계를 진행합니다. 이 모든 단계를 다 알아보기에는 너무 많음으로

### Compile steps - simply

간단하고 중요한 역할을 하는 3개의 단계만 보도록 하겠습니다.

### 코드 정적 분석 및 기존 경고 억제

첫 번째로 코드 정적 분석 및 기존 경고 억제 단계입니다. 대표적인 것들만 살펴 보자면 컴포저블 함수는 오직 컴포저블 함수 에서만 사용될 수 있게 제한합니다.

### 코드 정적 분석 및 기존 경고 억제 - 2

또한 Retention 이 Source 가 아닌 어노테이션을 인라인 람다에 사용하면 인라인되면서 어노테이션이 적용될 수 없어 경고가 발생하지만, Composable 어노테이션은 이를 허용합니다.

### 코드 정적 분석 및 기존 경고 억제 - 3

마지막으로 하나만 더 보자면 람다식은 메타데이터가 없어 항상 정렬된 인자를 요구하기 때문에 named arguments 를 허용하지 않습니다. 하지만 Composable 어노테이션이 붙은 람다는 이를 허용합니다.

### 코드 정적 분석 및 기존 경고 억제 - IDE Plugin

이 단계는 컴파일러의 프론트엔드에서 실행됨으로 적은 리소스 사용과 빠른 응답이 요구됩니다. 이를 위해 안드로이드 스튜디오에 번들로 설치된 Jetpack Compose 플러그인과 상호 작용 하며 진행됩니다.

### Composer 주입

다음 단계로 Composer 가 주입됩니다. Composer 는 컴포즈 런타임이 컴포저블과 상호 작용 할 수 있게 도와주는 인터페이스 입니다. 이 시점부터 컴포저블과 컴포즈 런타임이 연결됩니다.

### 컴포저블 그룹 생성

마지막 단계는 컴포저블 그룹 생성 입니다.

### Group

모든 컴포저블들은 슬릇 테이블에 저장된다고 설명했습니다.

### Group - 2

이 컴포저블이 표시하는 데이터인 Column Data 처럼, 표시하려는 데이터가 슬릇 테이블에 저장되기 까지의 모든 상위 컴포저블 컨테이너를 그룹 이라고 부릅니다.

그룹은 3가지로 나뉩니다.

### RestartableGroup

먼저 RestartableGroup 입니다. 이 그룹은 리컴포지션이 일어날 수 있는 컴포저블 주위로 생성됩니다.

별도의 옵션이 없다면 모든 컴포저블에 대해 생성되는 그룹이고, 리컴포지션 하는 방법을 가르칩니다. 모든 RestartableGroup 은 해당 범위 만큼 자체의 리컴포지션 스코프를 형성합니다. 이 리컴포지션 스코프를 컴포즈 내부에서는 RecomposeScope 라고 부릅니다.

### ReplaceableGroup

다음으로 ReplaceableGroup 입니다. 이 그룹은 분기에 따라 재배치 될 수 있는 컴포저블 주위로 생성됩니다.

그룹을 교체해야 할 때 슬릇 테이블에 저장된 데이터를 정리하는 방법을 가르칩니다.

### MovableGroup

마지막으로 MovableGroup 입니다. 컴포저블이 배치된 위치가 달라지면 위치 메모이제이션의 key 가 달라져서 슬릇 테이블에 저장된 데이터가 초기화됩니다. 데이터를 유지한 상태로 위치를 옮기기 위해선 key 컴포저블을 이용해 위치 메모이제이션에서 참고할 key 를 직접 정의해야 합니다.

key 컴포저블을 이용해 배치된 컴포저블은 주위로 MovableGroup 이 생성됩니다. MovableGroup 은 컴포저블이 위치 메모이제이션의 key 와 데이터를 항상 보존하며 이동하는 방법을 가르칩니다.

### 컴포저블 그룹 생성 - 2

이렇게 3가지 그룹이 생성되면서 이 발표에서 알아볼 컴파일러 단계 소개가 끝납니다.

### How it works? - Runtime

이어서 런타임에서 일어나는 마법도 보겠습니다.

### Compose 구조

컴포즈는 멀티 모듈로 구성된 프로젝트 입니다. 크게 Material, Foundation, UI, Runtime 으로 나뉘어 있으며, 이번 세션에는 Runtime 모듈을 설명합니다.

### Runtime Process (full)

런타임의 기초적인 흐름을 그래프로 나타내 보면 이렇게 꽤 크게 나옵니다. 이 그래프의 모든걸 살펴보기엔 최소 3시간은 필요하므로

### Runtime Process (simply)

런타임에서 필수적인 부분으로 요약해서 보겠습니다.

### setContent

우리는 컴포즈를 시작하기 위해 주로 setContent 를 사용합니다. 이 setContent 가 컴포즈 런타임과 UI 의 연결점이자 런타임 프로세스의 시작점 입니다.

### setContent - 2

setContent 는 내부에서 ComposeView 를 만들어서 다시 setContent 를 하고 있고, 이를 setContentView 로 보여주고 있습니다.

### ensureCompositionCreated

ComposeView 의 setContent 는 내부에서 여러 함수들을 거치면서 결국 ensureCompositionCreated 라는 함수를 실행합니다. 이 함수에서 또 다시 다른 setContent 를 호출하고 있으며 Recomposer 를 인자로 받게 됩니다.

### Recomposer

Recomposer 는 리컴포지션을 수행하고 하나 이상의 컴포저블에 업데이트를 적용하기 위한 스케줄러 입니다.

인자로는 CoroutineContext 를 받고 있고, AndroidUiDispatcher 가 주입됩니다.

### AndroidUiDispatcher

AndroidUiDispatcher 는 handler 콜백 또는 choreographer(코리아그래퍼) 의 애니메이션 프레임 단계 중 먼저 도래하는 단계에서 디스패치를 수행하는 CoroutineDispatcher 입니다. AndroidUiDispatcher 에서 choreographer 를 받고 있다는게 중요합니다. 추후 이게 어디서 사용되는지 설명하겠습니다.

### runRecomposeAndApplyChanges

Recomposer 가 생성되면 runRecomposeAndApplyChanges 함수가 실행됩니다. 이 함수는 recompositionRunner 로 작동되고 있습니다. recompositionRunner 를 보겠습니다.

### recompositionRunner

recompositionRunner 에서는 CoroutineContext 에서 MonotonicFrameClock 으로 MonotonicFrameClock 을 가져오고 있습니다. CoroutineContext 의 MonotonicFrameClock element 는 Recomposer 생성 과정에서 주입되며, Recomposer 의 안자로 받은 Choreographer 의 래퍼인 MonotonicFrameClock 를 반환합니다.

이후 MonotonicFrameClock 의 래퍼인 컴포즈 런타임에서 광역으로 사용되는 broadcastFrameClock 으로 스코프를 열어서 State 의 쓰기 이벤트를 받고, 인자로 받은 block 을 MonotonicFrameClock 과 함께 실행하고 있습니다.

### recompositionRunner - 2

State 의 쓰기 이벤트를 받으면 derive(드라이브)StateLocked 함수에 resume 을 해주고 있습니다. deriveStateLcoked 함수는 나중에 보고, runRecomposeAndApplyChanges 함수를 이어서 보겠습니다.

### runRecomposeAndApplyChanges - 2

runRecomposeAndApplyChanges 에서는 먼저 toRecompose 배열에 리컴포지션이 필요한 RecomposeScope 를 담고 있습니다.

이어서 Recomposer 가 리컴포지션 요청을 감지해야 하는 동안 반복하면서 새로운 리컴포지션 요청이 있다면 toRecompose 배열을 순회하며 performRecompose 함수를 실행함으로써 리컴포지션을 해주고 있습니다.

이 과정에서 performRecompose 가 MonotonicFrameClock 의 withFrameNanos 함수 안에서 실행되고 있다는게 중요합니다. MonotonicFrameClock 의 withFrameNanos 는 Choreographer 의 postFrameCallback 을 델리게이트 하고 있습니다. 이 부분 덕분에 컴포즈에서 UI 가 부드럽게 표시될 수 있는 것입니다.

새로운 리컴포지션 요청이 있을 때 까지 suspend 상태로 기다리는 awaitWorkAvailable 함수에 대해 보겠습니다.

### awaitWorkAvailable

간단하게 suspendCancellableCoroutine 를 이용하여 구현됐습니다. 대기중인 리컴포지션 요청이 있다면 바로 resume 하고 있고, 그렇지 않다면 workContinuation 으로 CancellableContinuation 저장 후 suspend 상태를 유지하게 됩니다.

### deriveStateLcoked

아까 건너뛰었던 deriveStateLocked 함수를 보면 현재 보류 중인 작업 여부에 따라 새로운 상태를 생성하고, 새로운 상태가 보류중인 작업이 있음을 나타낸다면 workContinuation 를 반환하고 있습니다.

### deriveStateLocked usage

이 deriveStateLocked 함수는 아까 스냅샷의 쓰기 이벤트 감지 부분에서 쓰이고 있었습니다.

### runRecomposeAndApplyChanges magic

이 부분을 통해 스냅샷에서 쓰기 이벤트가 감지된다면 workContinuation 을 resume 함으로써 리컴포지션이 진행됩니다. 이렇게 runRecomposeAndApplyChanges 함수가 끝나고, 컴포즈가 스냅샷의 변경 사항을 감지하고 리컴포지션을 진행하는 마법이 구현됩니다.

### ensureCompositionCreated - 2

이렇게 만들어진 Recomposer 가 ensureCompositionCreated 의 또 다른 setContent 로 주입됩니다. 이 setContent 는 Recomposer 를 받고 여러 과정을 다시 거치면서 Composer 를 생성하게 됩니다. 이후 해당 Composer 에서 doCompose 를 호출합니다.

### doCompose

doCompose 는 컴포지션 진행을 담당합니다. 먼저 startRoot 로 슬릇 테이블에서 루트 그룹을 시작하고 다른 필수 필드 및 구조를 초기화 하여 컴포저블이 그려지기 위한 조건을 만들어 줍니다.

다음으로 startGroup 으로 컴포저블이 들어갈 그룹을 열어주고 invokeComposble 로 컴포저블 함수를 실행함으로써 컴포지션을 진행합니다.

마지막으로 endGroup 과 endRoot 로 해당 컴포저블의 그룹을 닫고 있습니다.

컴포저블을 사용하는 content 는 람다식으로 content 를 배치하기 위해선 content 를 invoke 해서 content 람다를 실행하는 부분이 필요합니다. 이 역할을 invokeComposable 에서 해줍니다.

invokeComposable 은 content 람다에 인자 2개를 추가로 넣은 Function2 로 캐스팅하여 실행하고 있습니다. 컴파일러의 Composer 주입 단계에서 함수의 시그니처가 Function2 로 변경되기에 이런 캐스팅이 가능한 것입니다.

이렇게 런타임의 핵심 과정이 끝납니다.

이 invokeComposable 함수는 아까 runRecomposeAndApplyChanges 에서 리컴포지션을 하는데 사용됐던 performRecompose 에서도 최종적으로 사용되고, 이 이후로는 컴포저블이 방출되고 구체화 되는 UI 단계가 시작됩니다.

### 감사합니다!

지금까지 컴포즈의 컴파일러와 런타임에서 일어나는 마법을 모두 살펴보았습니다. 참 쉽죠?

이상으로 발표를 마치겠습니다. 감사합니다.

Ji Sungbin

April 29, 2023
Tweet

More Decks by Ji Sungbin

Other Decks in Programming

Transcript

  1. Jetpack Compose:


    How it works? ✨૑ࢿ࠼

    View full-size slide

  2. How it works?
    ↟$PNQJMFS
    ↟ 3VOUJNF

    View full-size slide

  3. /**


    * Composable য֢ప੉࣌਷ ஹನૉ ೣࣻীࢲ ೙ࣻ੸ਵ۽ ࢎਊغݴ ௼ѱ 3о૑੄ ৉ೡਸ ыणפ׮.


    *


    * 1. ݵ١ࢿ੄ ࠁ੢


    * 2. ਤ஖ ݫݽ੉ઁ੉࣌ ഝࢿച


    * 3. ؘ੉ఠ ઁҕ ژח ஹನ੷࠶ ߑ୹


    */


    @MustBeDocumented


    @Retention(AnnotationRetention.BINARY)


    @Target(


    AnnotationTarget.FUNCTION,


    AnnotationTarget.TYPE,


    AnnotationTarget.TYPE_PARAMETER,


    AnnotationTarget.PROPERTY_GETTER


    )


    annotation class Composable


    View full-size slide

  4. // э਷ inputਸ ߉਷ ஹನ੷࠶ਸ ݵ١ࢿ੄ ࠁ੢ਵ۽ ೦࢚ ੤प೯ਸ Ѥցګ׮.


    // ݵ١ࢿ: э਷ ো࢑੄ Ѿҗח ೦࢚ زੌೞ׮ח ࢿ૕


    @Composable


    fun Idempotent() {


    // ݅ড UI јन੉ ೙ਃೞ׮ݶ...


    Text(text = "ࢿ࠼ے٘") // ੉ ஹನ੷࠶਷ ೠ ߣ݅ प೯ػ׮.


    Text(text = System.currentTimeMillis().toString()) // ੉ ஹನ੷࠶਷ ೦࢚ ੤प೯ػ׮.


    }


    View full-size slide

  5. // ஹನ੷࠶਷ ۠ఋ੐ীࢲ Ѿ੿ػ ਋ࢶࣽਤী ݏѱ ߽۳۽ प೯ؽਵ۽ ஹನ੷࠶੄ प೯ ࣽࢲী ੄ઓೞח Ѥ ০૑ ঋ׮.


    // ٮۄࢲ ݵ١ࢿਸ ࠁ੢ೞח Ѫ੉ ઺ਃೞ׮.


    lateinit var text: String


    @Composable @Composable


    fun PureFunction() { fun LoadText() {


    LoadText() text = "Bye, world!"


    ShowText() Text(text = "Load Text...")


    } }


    @Composable


    fun ShowText() {


    Text(text = text) // text is not initialized properly оמࢿ ੓਺


    }


    View full-size slide

  6. // ஹನ੷࠶ ೣࣻ੄ दӒפ୊৬ call-site, ӒܻҊ प೯ػ ࣽࢲܳ ӝળਵ۽ Ҋਬೠ keyо ࢤࢿغҊ,


    // ஹನૉח ೧׼ keyܳ ӝળਵ۽ ݽٚ ؘ੉ఠܳ ੷੢ೞҊ ҙܻೠ׮.


    // э਷ keyܳ о૓ ஹನ੷࠶੉ э਷ inputਵ۽ ੤प೯ ػ׮ݶ ӝઓী प೯ػ Ѿҗܳ Ӓ؀۽ ੤ࢎਊೠ׮.


    // -> ݵ١ࢿҗ ਤ஖ ݫݽ੉ઁ੉࣌੄ Ѿҗ


    // ਤ஖ ݫݽ੉ઁ੉࣌: э਷ ਤ஖ীࢲ э਷ inputਵ۽ प೯غݶ ೦࢚ э਷ output੉ աৢ Ѫ੉ۄҊ о੿ೞҊ,


    // чਸ நयೞҊ ੤ࢎਊ ೞח ӝࣿ


    @Composable


    fun PositionMemoization() {


    repeat(5) {


    Text(text = "Bye, world!") // э਷ call-site ੉૑݅, ஹನ੷࠶ ؘ੉ఠо ׮ ׮ܲ ҕрী ੷੢ؽ


    }


    }


    View full-size slide

  7. /**


    * @Composable (input) -> output


    * ੉ ೣࣻ੄ ҃਋ input਷ ܻࣗझ ই੉٣, output਷ ܻࣗझ чܳ աఋշפ׮.


    *


    * ੉ ೣࣻ੄ @Composable਷ ؘ੉ఠܳ ઁҕೞח ৉ೡਸ ೤פ׮.


    */


    @Composable


    fun stringResource(@StringRes id: Int): String {


    val resources = resources()


    return resources.getString(id)


    }

    View full-size slide

  8. /**


    * @Composable (input) -> Unit


    * ੉ ೣࣻ੄ ҃਋ inputҗ output੉ হणפ׮.


    *


    * ੉ ೣࣻ੄ @Composable਷ ஹನ੷࠶ਸ ߑ୹ೞח ৉ೡਸ ೤פ׮.


    */


    @Composable


    fun ByeWorld() {


    Text(text = "Bye, world!")


    }


    View full-size slide

  9. /**


    * ஹನૉ੄ ݽٚ ؘ੉ఠܳ ੷੢ೞҊ ҙܻೞח ௿ېझੑפ׮.


    * Gap Buffer ੗ܐҳઑ৬ ےؒ ঘࣁझ۽ ҳഅعҊ, ղࠗ ؘ੉ఠח Arrayী ੷੢ؾפ׮.


    */


    internal class SlotTable : CompositionData, Iterable


    View full-size slide

  10. Gap Buffer: ࢎਊೡ ҕрٜਸ ޷ܻ ഛࠁ೧ فҊ, ೧׼ ҕрীࢲ ੘সਸ ૓೯ೞח ੗ܐ ҳઑ.


    ਤఃೖ٣ইীࢲח زੌೠ ਤ஖ Ӕ୊ী ௿۞झఠ݂ػ ബਯ੸ੋ ࢗੑ ߂ ࢏ઁ ੘সਸ ೲਊೞח ز੸ ߓৌ


    ੉ۄҊ ੿੄ೞҊ ੓णפ׮.

    View full-size slide

  11. Gap Buffer: زੌೠ ਤ஖ Ӕ୊ী ௿۞झఠ݂ػ ബਯ੸ੋ ࢗੑ ߂ ࢏ઁ ੘সਸ ೲਊೞח ز੸ ߓৌ


    1, ୡӝ Gap ࢤࢿ: O(N)


    [(v)_, _, _, _, _, _, _, _, _, _] // _ח Gap, (v)ח അ੤ cursorܳ աఋն

    View full-size slide

  12. Gap Buffer: زੌೠ ਤ஖ Ӕ୊ী ௿۞झఠ݂ػ ബਯ੸ੋ ࢗੑ ߂ ࢏ઁ ੘সਸ ೲਊೞח ز੸ ߓৌ


    1, ୡӝ Gap ࢤࢿ: O(N)


    [(v)_, _, _, _, _, _, _, _, _, _] // _ח Gap, (v)ח അ੤ cursorܳ աఋն


    2. 0ߣ૩ ੋؙझী Hi ࢗੑ


    [(v)H, i, _, _, _, _, _, _, _, _] // ݽف Gapী ࢗੑغ޲۽ ୶о Gap ࢤࢿ੉ ೙ਃೞ૑ ঋই O(1)ী ৮ܐؽ

    View full-size slide

  13. Gap Buffer: زੌೠ ਤ஖ Ӕ୊ী ௿۞झఠ݂ػ ബਯ੸ੋ ࢗੑ ߂ ࢏ઁ ੘সਸ ೲਊೞח ز੸ ߓৌ


    1, ୡӝ Gap ࢤࢿ: O(N)


    [(v)_, _, _, _, _, _, _, _, _, _] // _ח Gap, (v)ח അ੤ cursorܳ աఋն


    2. 0ߣ૩ ੋؙझী Hi ࢗੑ


    [(v)H, i, _, _, _, _, _, _, _, _] // ݽف Gapী ࢗੑغ޲۽ ୶о Gap ࢤࢿ੉ ೙ਃೞ૑ ঋই O(1)ী ৮ܐؽ


    3. 1ߣ૩ ੋؙझ੄ i ઁѢ


    [H, (v)_, _, _, _, _, _, _, _, _]


    // cursorܳ iо ੓ח ਤ஖۽ ৤ӝӝ ਤ೧ O(N)੉ ѦܻҊ


    // ੉റ Gapীࢲ ч ઁѢо ૓೯غ޲۽ O(1)݅ী ৮ܐؽ (ч ઁѢח ೧׼ чਸ Gapਵ۽ ߸҃ೞח Ѫਵ۽ ҳഅؾפ׮)

    View full-size slide

  14. Gap Buffer: زੌೠ ਤ஖ Ӕ୊ী ௿۞झఠ݂ػ ബਯ੸ੋ ࢗੑ ߂ ࢏ઁ ੘সਸ ೲਊೞח ز੸ ߓৌ


    1, ୡӝ Gap ࢤࢿ: O(N)


    [(v)_, _, _, _, _, _, _, _, _, _] // _ח Gap, (v)ח അ੤ cursorܳ աఋն


    2. 0ߣ૩ ੋؙझী Hi ࢗੑ


    [(v)H, i, _, _, _, _, _, _, _, _] // ݽف Gapী ࢗੑغ޲۽ ୶о Gap ࢤࢿ੉ ೙ਃೞ૑ ঋই O(1)ী ৮ܐؽ


    3. 1ߣ૩ ੋؙझ੄ i ઁѢ


    [H, (v)_, _, _, _, _, _, _, _, _]


    // cursorܳ iо ੓ח ਤ஖۽ ৤ӝӝ ਤ೧ O(N)੉ ѦܻҊ


    // ੉റ Gapীࢲ ч ઁѢо ૓೯غ޲۽ O(1)݅ী ৮ܐؽ (ч ઁѢח ೧׼ чਸ Gapਵ۽ ߸҃ೞח Ѫਵ۽ ҳഅؾפ׮)


    4. 0ߣ૩ ੋؙझ੄ H ܳ Bye ۽ ߸҃


    [(v)B, y, e, _, _, _, _, _, _, _]


    // cursorܳ Hо ੓ח ਤ஖۽ ৤ӝӝ ਤ೧ O(N)੉ ѦܻҊ


    // ੉റ ݽف Gapীࢲ ч সؘ੉౟о ૓೯غ޲۽ O(1)݅ী ৮ܐؽ

    View full-size slide

  15. var isLoading = false


    @Composable


    fun Main() {


    Column { // 1ߣ૩ ਤ஖ (അ੤ cursor)


    Text(text = "Column Data")


    if (isLoading) {


    Text(text = "Loading...")


    }


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Gap
    Gap
    ߣ૩ਤ஖

    View full-size slide

  16. var isLoading = true


    @Composable


    fun Main() {


    Column { // 1ߣ૩ ਤ஖ (੉੹ cursor)


    Text(text = "Column Data")


    if (isLoading) { // 2ߣ૩ ਤ஖ (അ੤ cursor)


    Text(text = "Loading...")


    }


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Text
    “Loading…”
    ߣ૩ਤ஖
    ߣ૩ਤ஖

    View full-size slide

  17. /**


    * Gapਸ ޖदೞҊ ౠ੿ ਤ஖੄ ੺؀੸ੋ ੋؙझܳ оܰఃח ېಌੑפ׮. (ےؒ ঘࣁझ ഝࢿച)


    *


    * ےؒ ঘࣁझ: ؘ੉ఠܳ ੷੢ೞח ࠶۾ਸ ೠߣী ৈ۞ ѐ ঘࣁझೞח Ѫ੉ ইפۄ


    * ౠ੿ ਤ஖۽ ߄۽ ੽Ӕೞৈ ೠ ߣী ೞա੄ ࠶۾ਸ ঘࣁझೞח ߑध


    */


    internal class Anchor(loc: Int) {


    internal var location = loc


    val valid get() = location != Int.MIN_VALUE


    fun toIndexFor(slots: SlotTable) = slots.anchorIndex(this)


    fun toIndexFor(writer: SlotWriter) = writer.anchorIndex(this)


    }

    View full-size slide

  18. var isLoading = true


    @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    if (isLoading) { // Anchor۽ ߄۽ ੽Ӕ


    Text(text = "Loading...")


    }


    }


    }


    Column
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Text
    “Loading…”
    "ODIPS۽߄۽੽Ӕ
    Text

    View full-size slide

  19. var isColumn = true


    @Composable


    fun Main() {


    if (isColumn) {


    Column {


    Text(text = "Column Data")


    }


    } else {


    Row {


    repeat(10) { index ->


    Text(text = "Row Data - $index")


    }


    }


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Gap
    Gap

    View full-size slide

  20. var isColumn = false


    @Composable


    fun Main() {


    if (isColumn) {


    Column {


    Text(text = "Column Data")


    }


    } else {


    Row {


    repeat(10) { index ->


    Text(text = "Row Data - $index")


    }


    }


    }


    }


    Row
    Text
    “Row Data - 0”
    Text
    “Row Data - 1”
    Text
    Text
    Text
    “Row Data - 2”
    “Row Data - 3”

    View full-size slide

  21. var isLoading = false


    @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    if (isLoading) {


    Text(text = "Loading...")


    }


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Gap
    Gap
    Composition

    View full-size slide

  22. var isLoading = true


    @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    if (isLoading) {


    Text(text = "Loading...")


    }


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Text
    “Loading…”
    Recomposition

    View full-size slide

  23. var isLoading = true


    @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    if (isLoading) {


    Text(text = "Loading...")


    }


    }


    }


    Emit
    Column
    Text
    Text

    View full-size slide

  24. var isLoading = true


    @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    if (isLoading) {


    Text(text = "Loading...")


    }


    }


    }


    Materializing

    View full-size slide

  25. var isLoading = true


    @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    if (isLoading) {


    Text(text = "Loading...")


    }


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Text
    “Loading…”

    View full-size slide

  26. val isLoading = mutableStateOf(false) // Snapshot System


    isLoading.value = true


    @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    if (isLoading) {


    Text(text = "Loading...")


    }


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Text
    “Loading…”

    View full-size slide

  27. How it works?
    ↟$PNQJMFS
    ↟ 3VOUJNF

    View full-size slide

  28. @Composable


    fun TextWrapper() {


    Text(text = "SungbinLand")


    }


    dependencies {


    // annotation processor XXX


    implementation "androidx.compose.foundation:foundation:1.3.0-alpha01"


    }

    View full-size slide

  29. @Composable


    fun TextWrapper() {


    Text(text = "SungbinLand")


    }


    buildFeatures {


    compose true // ஹನૉ ௏ౣܽ ஹ౵ੌ۞ ഝࢿച


    /*


    * ஹನૉ ஹ౵ੌ җ੿ਸ ௏ౣܽ ঱য੄ ஹ౵ੌ җ੿ਵ۽ ੐ߓ٘ೞৈ ୊ܻ ࣘبܳ ֫੉Ҋ,


    * য֢ప੉࣌ ೐۽ࣁࢲח ೡ ࣻ হח ֫ਸ ࣻળ੄ ௏٘ ੽Ӕਸ ೞৈ ஹ౵ੌؾפ׮.


    * ৘ܳ ٜয, ௏ౣܽҗ ஹನૉח ݣ౭೒ۖಬਸ ؀࢚ਵ۽ ೞ޲۽ ஹ౵ੌ җ੿ীࢲ ࢤࢿغח IRਸ


    * അ੤ ೒ۖಬী ݏח ജ҃ਵ۽ ࣻ੿ೞҊ loweringਸ ૓೯೤פ׮.


    */


    }


    // or…


    dependencies {


    "kotlinCompilerPluginClasspath"("org.jetbrains.compose.compiler:compiler:$version")


    }


    View full-size slide

  30. ௏ౣܽஹ౵ੌ۞ߡ੹୓௼ ௏٘੿੸࠙ࢳ߂ӝઓ҃Ҋরઁ ஹನૉ۠ఋ੐ߡ੹୓௼
    *3ࢤࢿ
    ௿ېझউ੿ࢿ୶ۿ
    -JWF-JUFSBMഝࢿച
    ۈ׮୭੸ച $PNQPTFS઱ੑ ࠺Ү੹౵ഝࢿച
    %FGBVMU"SHVNFOUT੤ҳഅ
    ஹನ੷࠶Ӓܛࢤࢿ
    ,MJC EFDPZࢤࢿ

    View full-size slide

  31. ௏ౣܽஹ౵ੌ۞ߡ੹୓௼ ௏٘੿੸࠙ࢳ߂ӝઓ҃Ҋরઁ ஹನૉ۠ఋ੐ߡ੹୓௼
    *3ࢤࢿ
    ௿ېझউ੿ࢿ୶ۿ
    -JWF-JUFSBMഝࢿച
    ۈ׮୭੸ച $PNQPTFS઱ੑ ࠺Ү੹౵ഝࢿച
    %FGBVMU"SHVNFOUT੤ҳഅ
    ஹನ੷࠶Ӓܛࢤࢿ
    ,MJC EFDPZࢤࢿ

    View full-size slide

  32. ௏٘੿੸࠙ࢳ߂ӝઓ҃Ҋরઁ j j

    View full-size slide

  33. ௏٘੿੸࠙ࢳ߂ӝઓ҃Ҋরઁ j j

    View full-size slide

  34. ௏٘੿੸࠙ࢳ߂ӝઓ҃Ҋরઁ j j

    View full-size slide

  35. $PNQPTFS઱ੑ
    j j
    // ஹ౵ੌ ੹


    @Composable


    fun TextWrapper() {


    Text(text = "SungbinLand")


    }


    // ஹ౵ੌ റ, Composer: ஹನૉ ۠ఋ੐੉ ஹನ੷࠶җ ࢚ഐ ੘ਊ ೡ ࣻ ੓ѱ ب৬઱ח ੋఠಕ੉झ


    @Composable


    fun TextWrapper(composer: Composer, changed: Int) {


    composer.startRestartGroup(-137853230)


    composer.sourceInformation("C(TextWrapper)22@513L26:MainActivity.kt#dmu2em")


    if (changed == 0 && composer.skipping) {


    composer.skipToGroupEnd()


    } else {


    Text(text = "SungbinLand")


    }


    composer.endRestartGroup()?.updateScope { composer, _ ->


    TextWrapper(composer, changed or 1)


    }


    }


    View full-size slide

  36. @Composable


    fun TextWrapper(composer: Composer, changed: Int) {


    composer.startRestartGroup(-137853230)


    composer.sourceInformation("C(TextWrapper)22@513L26:MainActivity.kt#dmu2em")


    if (changed == 0 && composer.skipping) {


    composer.skipToGroupEnd()


    } else {


    Text(text = "SungbinLand")


    }


    composer.endRestartGroup()?.updateScope { composer, _ ->


    TextWrapper(composer, changed or 1)


    }


    } Group
    ஹನ੷࠶Ӓܛࢤࢿ
    j j

    View full-size slide

  37. @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    }


    }


    Column
    Text
    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Gap
    Gap

    View full-size slide

  38. @Composable


    fun Main() {


    Column {


    Text(text = "Column Data")


    }


    }


    “Column Data”
    Gap
    Gap
    Gap
    Gap
    Gap
    Gap
    Group
    Group

    View full-size slide

  39. // RestartableGroup: ܻஹನ૑࣌੉ ੌযզ ࣻ ੓ח ஹನ੷࠶


    // ઱ਤ۽ ࢤࢿػ׮. ߹ب੄ ২࣌੉ হ׮ݶ ݽٚ ஹನ੷࠶ী


    // ࢤࢿغח Ӓܛ੉Ҋ, ܻஹನ૑࣌ ೞח ߑߨਸ оܰ஘׮.


    // ݽٚ RestartableGroup਷ ೧׼ ߧਤ ݅ఀ


    // ੗୓੄ ܻஹನ૑࣌ झ௏೐(RecomposeScope)ܳ ഋࢿೠ׮.


    @Composable


    fun RestartableContainer() {


    Text(text = "SungbinLand")


    }


    RestartableGroup
    “SungbinLand”
    Gap
    Gap
    Gap
    Gap
    Gap
    Gap
    RestartableGroup
    3FTUBSUBCMF$POUBJOFS
    3FDPNQPTF4DPQF 5FYU
    3FDPNQPTF4DPQF

    View full-size slide

  40. // ReplaceableGroup: ࠙ӝী ٮۄ ੤ߓ஖ ؼ ࣻ ੓ח


    // ஹನ੷࠶ ઱ਤ۽ ࢤࢿػ׮.


    // Ӓܛਸ Ү୓೧ঠ ೡ ٸ ੘ࢿػ ؘ੉ఠܳ ੿ܻೞח ߑߨਸ оܰ஘׮.


    @Composable


    fun ReplaceableContainer(isColumn: Boolean = true) {


    when (isColumn) {


    true -> Text(text = "Column")


    else -> Text(text = "Row")


    }


    }


    RestartableGroup
    “Column”
    Gap
    Gap
    Gap
    Gap
    Gap
    ReplaceableGroup
    3FQMBDFBCMF$POUBJOFS XIFO
    RestartableGroup
    5FYU

    View full-size slide

  41. /**


    * ஹನ੷࠶੉ ߓ஖ػ ਤ஖о ׳ۄ૑ݶ ਤ஖ ݫݽ੉ઁ੉࣌੄ keyо ׳ۄઉࢲ ؘ੉ఠо ୡӝചؾפ׮.


    * ؘ੉ఠܳ ਬ૑ೠ ࢚క۽ ਤ஖ܳ ৤ӝӝ ਤ೧ࢶ key ஹನ੷࠶ਸ ੉ਊ೧


    * ਤ஖ ݫݽ੉ઁ੉࣌ীࢲ ଵҊೡ keyܳ ૒੽ ੿੄೧ঠ ೤פ׮.


    *


    * key ஹನ੷࠶ਸ ੉ਊ೧ ߓ஖ػ ஹನ੷࠶਷ ઱ਤ۽ MovableGroup੉ ࢤࢿؾפ׮.


    * MovableGroup਷ ஹನ੷࠶੉ ਤ஖ ݫݽ੉ઁ੉࣌ key৬ ؘ੉ఠܳ ೦࢚ ࠁઓೞݴ


    * ੉زೞח ߑߨਸ оܰ஝פ׮.


    */


    @Composable


    inline fun key(


    vararg keys: Any?,


    block: @Composable () -> T


    )


    @Composable


    fun MovableContainer() {


    key(Any()) {


    Text(text = "Key")


    }


    }


    RestartableGroup
    “Key”
    Gap
    Gap
    Gap
    Gap
    Gap
    MovableGroup
    .PWBCMF$POUBJOFS LFZ
    RestartableGroup
    5FYU

    View full-size slide

  42. ↟3FTUBSUBCMF(SPVQ
    ↟3FQMBDFBCMF(SPVQ
    ↟.PWBCMF(SPVQ
    ஹನ੷࠶Ӓܛࢤࢿ
    j j

    View full-size slide

  43. How it works?
    ↟$PNQJMFS
    ↟3VOUJNF

    View full-size slide

  44. 🎨
    🖼️
    🖌️
    🧰
    Material
    ݠఠܻ঴٣੗ੋदझమ
    5FYU #VUUPO #PUUPN4IFFU $IJQ "MFSU%JBMPH *DPOT 4VSGBDF
    Foundation
    о੢ӝୡ੸ੋ٣੗ੋ
    -B[Z-JTU $PMVNO $BOWBT 4IBQF (FTUVSFT 0WFSTDSPMM&GGFDU
    UI
    ஹನૉ6*5PPMLJU੄ӝࠄࢸ҅
    -BZPVU %JBMPH $PNQPTF7JFX"EBQUFS 1SFWJFX"DUJWJUJZ
    Runtime
    ஹನૉউ٘۽੉٘োѾ
    *OWBMJEBUJPO 4OBQTIPU4ZTUFN 4MPU5BCMF -JWF-JUFSBM

    View full-size slide

  45. Runtime Process (full)

    View full-size slide

  46. Runtime Process (simply)

    View full-size slide

  47. class MainActivity : ComponentActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    Text(text = "SungbinLand")


    }


    }


    }


    View full-size slide

  48. // ஹನૉ ۠ఋ੐җ UI੄ োѾ


    fun ComponentActivity.setContent(


    parent: CompositionContext? = null,


    content: @Composable () -> Unit


    ) {


    ComposeView(this).apply {


    setParentCompositionContext(parent)


    setContent(content)


    setOwners()


    setContentView(this, DefaultActivityContentLayoutParams)


    }


    }


    View full-size slide

  49. // ComposeView#setContent۽ ੉ ೣࣻо प೯ؽ


    private fun ensureCompositionCreated() {


    setContent(/* Recomposerо ٜ݅য૑Ҋ ઱ੑؾפ׮ */) {


    content()


    }


    }

    View full-size slide

  50. /**


    * ܻஹನ૑࣌ਸ ࣻ೯ೞҊ ೞա ੉࢚੄ ஹನ੷࠶ী সؘ੉౟ܳ ੸ਊೞӝ ਤೠ झா઴۞ ੑפ׮.


    */


    class Recomposer(


    effectCoroutineContext: CoroutineContext


    ) : CompositionContext()


    /**


    * Handler ௒ߔ ژח Choreographer੄ গפݫ੉࣌ ೐ۨ੐ ױ҅ ઺


    * ݢ੷ بېೞח ױ҅ীࢲ ٣झಁ஖ܳ ࣻ೯ೞח CoroutineDispatcher ੑפ׮.


    */


    class AndroidUiDispatcher private constructor(


    val choreographer: Choreographer,


    private val handler: Handler


    ) : CoroutineDispatcher()


    View full-size slide

  51. /**


    * ܻஹನ૑࣌ਸ ࣻ೯ೞҊ ೞա ੉࢚੄ ஹನ੷࠶ী সؘ੉౟ܳ ੸ਊೞӝ ਤೠ झா઴۞ ੑפ׮.


    */


    class Recomposer(


    effectCoroutineContext: CoroutineContext


    ) : CompositionContext()


    /**


    * Handler ௒ߔ ژח Choreographer੄ গפݫ੉࣌ ೐ۨ੐ ױ҅ ઺


    * ݢ੷ بېೞח ױ҅ীࢲ ٣झಁ஖ܳ ࣻ೯ೞח CoroutineDispatcher ੑפ׮.


    */


    class AndroidUiDispatcher private constructor(


    val choreographer: Choreographer,


    private val handler: Handler


    ) : CoroutineDispatcher()


    View full-size slide

  52. suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->


    val toRecompose = mutableListOf()


    while (shouldKeepRecomposing) {


    awaitWorkAvailable()


    parentFrameClock.withFrameNanos { frameTime ->


    try {


    toRecompose.fastForEach { composition ->


    performRecompose(composition)


    }


    } finally {


    toRecompose.clear()


    }


    }


    }


    }


    View full-size slide

  53. private suspend fun recompositionRunner(


    block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit


    ) {


    // Recomposerীࢲ ߉਷ Choreographer੄ ېಌੋ MonotonicFrameClockਸ оઉৡ׮.


    val parentFrameClock: MonotonicFrameClock = coroutineContext.monotonicFrameClock


    // broadcastFrameClock: ஹನૉ ۠ఋ੐ীࢲ ҟ৉ਵ۽ ࢎਊغח MonotonicFrameClock੄ ېಌ


    withContext(broadcastFrameClock) {


    Snapshot.registerApplyObserver { _, _ -> // State੄ write ੉߮౟ܳ ߉ח׮.


    deriveStateLocked()?.resume(Unit)


    }


    coroutineScope {


    block(parentFrameClock)


    }


    }


    }


    View full-size slide

  54. private suspend fun recompositionRunner(


    block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit


    ) {


    // Recomposerীࢲ ߉਷ Choreographer੄ ېಌੋ MonotonicFrameClockਸ оઉৡ׮.


    val parentFrameClock: MonotonicFrameClock = coroutineContext.monotonicFrameClock


    // broadcastFrameClock: ஹನૉ ۠ఋ੐ীࢲ ҟ৉ਵ۽ ࢎਊغח MonotonicFrameClock੄ ېಌ


    withContext(broadcastFrameClock) {


    Snapshot.registerApplyObserver { _, _ -> // State੄ write ੉߮౟ܳ ߉ח׮.


    deriveStateLocked()?.resume(Unit)


    }


    coroutineScope {


    block(parentFrameClock)


    }


    }


    }


    View full-size slide

  55. suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->


    // ܻஹನ૑࣌੉ ೙ਃೠ RecomposeScopeо ׸ӟ׮. (׸ח җ੿ ࢤۚ)


    val toRecompose = mutableListOf()


    // shouldKeepRecomposing: Recomposerо ܻஹನ૑࣌ ਃ୒ਸ х૑೧ঠ ೞח૑ܳ աఋմ׮.


    while (shouldKeepRecomposing) {


    awaitWorkAvailable() // ࢜۽਍ ܻஹನ૑࣌ ਃ୒੉ ੓ਸ ٸ ө૑ suspend ࢚క۽ ӝ׮ܽ׮.


    parentFrameClock.withFrameNanos { frameTime -> // Choreographer#postFrameCallbackਸ ഐ୹ೠ׮.


    try {


    toRecompose.fastForEach { composition -> // RecomposeScopeܳ ࣽഥೞݴ


    performRecompose(composition) // ܻஹನ૑࣌ ૓೯!


    }


    } finally {


    toRecompose.clear()


    }


    }


    }


    }


    View full-size slide

  56. private suspend fun awaitWorkAvailable() {


    suspendCancellableCoroutine { co ->


    if (hasSchedulingWork) { // ؀ӝ઺ੋ ܻஹನ૑࣌ ਃ୒੉ ੓׮ݶ ߄۽ resume ೞҊ


    co.resume(Unit)


    } else {


    // ؀ӝ઺ੋ ܻஹನ૑࣌ ਃ୒੉ হ׮ݶ


    // workContinuationਵ۽ CancellableContinuation ੷੢ റ suspend ਬ૑


    workContinuation = co


    }


    }


    }


    View full-size slide

  57. private fun deriveStateLocked(): CancellableContinuation? {


    // ࠁܨ ઺ੋ ੘স: ܻஹನ૑࣌ ਃ୒ ١١ ஹನૉ ۠ఋ੐ ղࠗীࢲ ૓೯غח ৈ۞ ੘স ਃ୒


    val newState = when { /* അ੤ ࠁܨ ઺ੋ ੘স ৈࠗী ٮۄ ࢜۽਍ ࢚క ҅࢑ */ }


    return if (newState == State.PendingWork) { // ࠁܨ ઺ੋ ੘স੉ ੓׮ݶ


    workContinuation.also { // workContinuation ߈ജ ߂ workContinuation ୡӝച


    workContinuation = null


    }


    } else null


    }


    View full-size slide

  58. private suspend fun recompositionRunner(


    block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit


    ) {


    val parentFrameClock = coroutineContext.monotonicFrameClock


    withContext(broadcastFrameClock) {


    Snapshot.registerApplyObserver { _, _ ->


    deriveStateLocked()?.resume(Unit)


    }


    coroutineScope {


    block(parentFrameClock)


    }


    }


    }


    View full-size slide

  59. suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->


    val toRecompose = mutableListOf()


    while (shouldKeepRecomposing) {


    awaitWorkAvailable() // ܻஹನ૑࣌ ਃ୒੉ ੓ਸ ٸ ө૑ suspend


    parentFrameClock.withFrameNanos { frameTime -> // ׮਺ ೐ۨ੐ীࢲ


    try {


    toRecompose.fastForEach { composition -> performRecompose(composition) } // Recomposition!


    } finally {


    toRecompose.clear()


    }


    }


    }


    }


    private suspend fun recompositionRunner(block: suspend CoroutineScope.(MonotonicFrameClock) -> Unit) {


    val parentFrameClock = coroutineContext.monotonicFrameClock


    withContext(broadcastFrameClock) {


    Snapshot.registerApplyObserver { _, _ -> deriveStateLocked()?.resume(Unit) } // State ч ߸҃੉ ੓ਸ ٸ resume


    coroutineScope { block(parentFrameClock) }


    }


    }


    View full-size slide

  60. // ComposeView#setContent۽ ੉ ೣࣻо प೯ؽ


    private fun ensureCompositionCreated() {


    setContent(/* Recomposerо ٜ݅য૑Ҋ ઱ੑؾפ׮ */) {


    content()


    }


    }

    View full-size slide

  61. // this: Composer


    private fun doCompose(content: @Composable () -> Unit) {


    startRoot() // ठܶ ప੉࠶ ୡӝച


    startGroup() // ஹನ੷࠶੉ ٜযт Ӓܛ ࢤࢿ


    invokeComposable(this, content)


    endGroup()


    endRoot()


    }


    // performRecomposeب ѾҴ ੉ ೣࣻܳ प೯ೠ׮.


    internal actual fun invokeComposable(


    composer: Composer,


    composable: @Composable () -> Unit


    ) {


    val realFn = composable as Function2


    realFn(composer, 1)


    }


    Root
    Group
    Composable
    Gap
    Gap
    Gap
    Gap
    Gap
    Gap

    View full-size slide

  62. хࢎ೤פ׮
    ஹನૉ ղࠗ ௏٘ח cs.android.comীࢲ оઉ৳णפ׮.


    ੉ ೐ۨઃప੉࣌ীח షझ౱ীࢲ ઁҕೠ షझಕ੉झо ੸ਊغয ੓णפ׮.


    QnA🙋

    View full-size slide