컴포저블 함수는 트리(tree)로 구성된다. 이 때 상태는 일반적으로 트리에서 가능한 한 가장 높은 노드에 선언되어야 한다(상태 호이스팅). 그리고 일반적으로 이렇게 선언된 상태는 하위 트리로 전달되곤 한다. 하지만 트리에서 여러 단계를 통해 상태를 전달해야 할 때는 다소 번거로울 수 있다.

예를 들어보자. 우선 컴포저블 함수 트리의 깊이(depth)가 n개라고 가정한다. 최상위 노드로부터 상태를 최하위 노드까지 상태를 전달해야한다면, 모든 컴포저블 함수에 상태를 전달하기 위한 n개의 매개변수를 추가해야 한다. 깊이가 100이라면 함수가 100개이므로 매개변수를 100개 추가해야한다.

CompositionLocal을 활용해 위 문제를 단순하게 해결할 수 있다.

CompositionLocal의 이해

CompositionLocal은 컴포저블 트리 상위에서 선언된 상태를 하위에서 접근할 수 있는 방법을 제공한다.

그림처럼 Composable4 한정으로 CompositionLocal의 범위를 지정했다면, Composition4,5,6만 선언된 상태에 접근할 수 있다.

CompositionLocal 이용하기

CompositionLocal을 이용하는 방법을 단계별로 살펴본다.

1. CompositionLocal 생성하기

CompositionLocal은 상태를 제공하는 일종의 컨테이너와 같다. CompositionLocal을 만드는 2가지 방법은 다음과 같다.

// StaticProvidableCompositionLocal로 만들기
val staticCompositionLocal = staticCompositionLocalOf {
    ${상태}
}

// DynamicProvidableCompositionLocal로 만들기
val dynamicCompositionLocal = compositionLocalOf {
    ${상태}
}

두 CompositionLocal 차이는 이 포스팅 아래에서 다시 다루도록 하겠다.

2. CompositionLocalProvider로 컴포저블 함수 감싸기

상위 컴포저블 함수부터 하위까지 CompositionLocal을 제공하기 위해 다음과 같이 CompositionLocalProvider를 사용한다.

위의 그림을 예로 들어 하위로 전달하고자 하는 것이 빨간색(상태)이라고 가정한다면 다음과 같이 코드를 작성할 수 있다.

val ColorCompositionLocal = staticCompositionLocalOf {
    Color.Blue // 기본값을 정의
}

@Composable
fun Composable1() {
    ...
    CompositionLocalProvider(ColorCompositionLocal.provides(Color.Red)) {
        Composable4()
    }
}

@Composable
fun Composable4() {
    ...
}

CompositionLocalProvider 함수 내부를 살펴보면 알겠지만, CompositionLocal은 1개 이상 제공할 수 있다.

fun CompositionLocalProvider(
    vararg values: ProvidedValue<*>, 
    content: @Composable () -> Unit)

3. CompositionLocal로 부터 값(상태) 읽기

CompositionLocalProvider로 감싼 하위 컴포저블함수들은 이제 CompositionLocal로부터 현재 값을 읽을 수 있다.

@Composable
fun Composable3() {
    Text(
        modifier = Modifier.background(color = ColorCompositionLocal.current), // 빨간색
        text = "Composable3"
    )
    ...
}

CompositionLocal 예제코드

상술한 트리 및 다음 예제코드를 통해 CompositionLocal에 대한 통찰을 얻을 수 있다.

val ColorCompositionLocal = staticCompositionLocalOf {
    Color.Blue // 기본값을 정의 한다
}

@Composable
fun Composable1() {
    Column {
        Text(
            modifier = Modifier.background(color = ColorCompositionLocal.current),
            text = "Composable1"
        )
        CompositionLocalProvider(ColorCompositionLocal.provides(Color.Cyan)) {
            Composable2()
        }
        CompositionLocalProvider(ColorCompositionLocal.provides(Color.Red)) {
            Composable4()
        }
    }

}

@Composable
fun Composable2() {
    Text(
        modifier = Modifier.background(color = ColorCompositionLocal.current),
        text = "Composable2"
    )
    Composable3()
}

@Composable
fun Composable3() {
    Text(
        modifier = Modifier.background(color = ColorCompositionLocal.current),
        text = "Composable3"
    )
}

@Composable
fun Composable4() {
    Text(
        modifier = Modifier.background(color = ColorCompositionLocal.current),
        text = "Composable4"
    )

    CompositionLocalProvider(ColorCompositionLocal.provides(Color.Green)) {
        Composable5()
    }
    Composable6()
}
@Composable
fun Composable5() {
    Text(
        modifier = Modifier.background(color = ColorCompositionLocal.current),
        text = "Composable5"
    )
}
@Composable
fun Composable6() {
    Text(
        modifier = Modifier.background(color = ColorCompositionLocal.current),
        text = "Composable6"
    )
}

@Preview(showBackground = true)
@Composable
private fun DefaultPreview() {
    Composable1()
}

staticCompositionLocalOf와 compositionLocalOf의 차이

staticCompositionLocalOf() 함수는 자주 변경되지 않는 상태를 저장할 때 이용하면 좋다. 상태가 변경되면 해당 상태가 할당된 노드의 하위 노드를 모두 재구성해야하기 때문이다. 한편 compositionLocalOf() 함수는 현재 상태에 접근하는 컴포저블에 대해서만 재구성을 수행한다. 이 함수는 변경이 잦은 상태를 다룰 때 이용해야 한다.

다음 예제는 컴포저블 함수의 계층별로 재구성이 발생하는 시점에 카운트를 증가시키고 해당 카운트를 보여준다. 이 예제를 통해 Static CompositionLocal과 Dynamic CompositionLocal의 차이를 알 수 있다.

var color by mutableStateOf(Color.Red)

private var outsideStatic = 0
private var centerStatic = 0
private var insideStatic = 0

private var outsideDynamic = 0
private var centerDynamic = 0
private var insideDynamic = 0

private val ColorComposableLocalStatic = staticCompositionLocalOf<Color> { error("기본값 없음") }

private val ColorComposableLocalDynamic = compositionLocalOf<Color> { error("기본값 없음") }

@Composable
fun CompositionLocals() {
    Column {
        Text("staticCompositionLocalOf")
        CompositionLocalProvider(ColorComposableLocalStatic provides color) {
            outsideStatic++
            MyBox(color = Color.Yellow, outsideStatic, centerStatic, insideStatic) {
                centerStatic++
                MyBox(color = ColorComposableLocalStatic.current, outsideStatic, centerStatic, insideStatic) {
                    insideStatic++
                    MyBox(color = Color.Yellow, outsideStatic, centerStatic, insideStatic) {
                    }
                }
            }
        }

        Text("compositionLocalOf")
        CompositionLocalProvider(ColorComposableLocalDynamic provides color) {
            outsideDynamic++
            MyBox(color = Color.Yellow, outsideDynamic, centerDynamic, insideDynamic) {
                centerDynamic++
                MyBox(color = ColorComposableLocalDynamic.current, outsideDynamic, centerDynamic, insideDynamic) {
                    insideDynamic++
                    MyBox(color = Color.Yellow, outsideDynamic, centerDynamic, insideDynamic) {
                    }
                }
            }
        }

        Button(onClick = {
            color = if (color == Color.Green) {
                Color.Red
            } else {
                Color.Green
            }
        }, modifier = Modifier.fillMaxWidth()) {
            Text("Click Me")
        }
    }

}

@Composable
fun MyBox(color: Color,
          outside: Int,
          center: Int,
          inside: Int,
          content: @Composable BoxScope.() -> Unit) {
    Column (Modifier.background(color)) {
        Text("outside = $outside, center = $center, inside = $inside")
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            content = content
        )
    }
}

@Preview(showBackground = true)
@Composable
private fun CompositionLocalsPreview() {
    CompositionLocals()
}


후원하기

카테고리: Compose

1개의 댓글

최우성 · 2023년 1월 18일 1:43 오후

2분만에 따끈따끈한 글.. 감사합니다 찰스님

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.