2장. 컨트롤 플로우 그룹 생성

Compiler는 Composable 함수의 본문에 group을 삽입한다.

Composable 함수는 런타임 시에 그룹을 생성하며, 생성된 그룹들은 Composable 함수의 현 상태에 대한 모든 정보를 감싸고 보존한다. 이를 통해 composition은 그룹의 교체가 필요할때, 정체성을 유지하며 데이터를 이동시킬때, recomposition중에 함수를 재시작할 때 쓰여진 데이터를 어떻게 처리해야 할지 알 수 있다.

또한, 그룹은 Composable 함수의 호출 위치에 대한 정보를 가지고 있다. 이를 통해 그룹을 저장하고, 위치 기억법을 가능하게 한다.

교체 가능한 그룹 (ReplaceableGroup)

Composable 람다식을 기억할때 Composable 람다식의 본문에 $composer, $key, 람다식의 실체와 같은 값을 매개변수로 받는 Composable 팩토리 함수 호출을 삽입해 composable 람다식이 자동으로 감싸진다고 했다.

@Suppress("unused")
@ComposeCompilerApi
public fun composableLambda(
    composer: Composer,
    key: Int,
    tracked: Boolean,
    block: Any,
): ComposableLambda {
    // 함수의 키와 람다의 키가 중복되는 것을 피하기 위해, (원본 키를) 변형한(rolled) 버전의 키를 사용합니다.
    // 이는 그룹이 키 번호에 의해 무효화(invalidated)되는 Live Edit 시나리오에서 특히 중요합니다.
    // 이를 통해 함수가 무효화되더라도 그 내부의 람다까지 같이 무효화되지 않도록 보장합니다.
    composer.startMovableGroup(key.rol(1), lambdaKey)
    val slot = composer.rememberedValue()
    val result =
        if (slot === Composer.Empty) {
            val value = ComposableLambdaImpl(key, tracked, block)
            composer.updateRememberedValue(value)
            value
        } else {
            slot as ComposableLambdaImpl
            slot.update(block)
            slot
        }
    composer.endMovableGroup()
    return result
}

private val lambdaKey = Any()

먼저 key로 이동 가능한 그룹을 시작하고, 중간에 모든 텍스트의 범위를 감싸고, 마지막으로 그룹을 닫는다. 시작과 끝 호출 사이에서, composition을 관련성 있는 정보로 업데이트한다.

위의 예시처럼 Composable 람다식뿐만 아니라 다른 Composable 함수의 호출 또한 동일하게 처리된다.

@NonRestartableComposable
@Composable
fun Foo(x: Int) {
    Wat()
}

// Decompile
public static final void Foo(int x, @Nullable Composer $composer, int $changed) {
		$composer.startReplaceGroup(-1621382871);
		Wat($composer, 0)
		$composer.endReplaceGroup();
}

위의 Composable 함수 또한 composition에 저장하기 위한 교체 가능한 그룹을 생성한다. 그룹은 일종의 트리와 같다. 각 그룹은 원하는만큼 그 어떠한 수의 그룹도 자식으로 가질 수 있다. 위의 코드에서 Wat() 함수 또한 Composable이라면, 컴파일러는 해당 함수에 대해서도 그룹을 삽입할 것이다.

$composer.startReplaceableGroup(key_foo) // Foo의 시작
    // Foo의 로직
    $composer.startReplaceableGroup(key_wat) // Wat의 시작
        // Wat의 로직 (예: Text 호출 등)
        $composer.startReplaceableGroup(key_text) 
        $composer.endReplaceableGroup()
    $composer.endReplaceableGroup() // Wat의 끝
$composer.endReplaceableGroup() // Foo의 끝

Composable 호출은 위치에 기반하기 때문에 정체성이 보존될 수 있다고 “Composable 어노테이션” 섹션에서 살펴보았다. 런타임은 아래의 서로 다른 두 Text 호출이 다르다는 것을 이해할 수 있다.

@Composable
fun ConditionText() {
    if (condition) {
        Text(text = "Hello")
    } else {
        Text(text = "World")
    }
}

// Decompile
if (condition) {
    $composer.startReplaceGroup(15449722);
    ComposerKt.sourceInformation($composer, "21@449L20");
    TextKt.Text--4IGK_g("Hello", ...);
    $composer.endReplaceGroup();
 } else {
    $composer.startReplaceGroup(15491386);
    ComposerKt.sourceInformation($composer, "23@491L20");
    TextKt.Text--4IGK_g("World", ...);
    $composer.endReplaceGroup();
 }

이와 같은 조건부 논리를 수행하는 Composable 함수 또한 교체 가능한 그룹을 발행하는데, 조건이 전환되면 교체될 수 있는 그룹을 슬롯 테이블에 저장한다.