Compose는 일반적인 동작을 위한 다양한 수정자를 기본적으로 제공하지만, 자체 맞춤 수정자를 만들 수도 있습니다.
수정자에는 여러 부분이 있습니다.
- 수정자 팩토리
- 이는 수정자를 위한 관용적인 API를 제공하고 수정자를 함께 연결할 수 있도록 하는
Modifier의 확장 함수입니다. 수정자 팩토리는 Compose에서 UI를 수정하는 데 사용하는 수정자 요소를 생성합니다.
- 이는 수정자를 위한 관용적인 API를 제공하고 수정자를 함께 연결할 수 있도록 하는
- 수정자 요소
- 여기에서 수정자의 동작을 구현할 수 있습니다.
필요한 기능에 따라 맞춤 수정자를 구현하는 방법은 여러 가지가 있습니다. 맞춤 수정자를 구현하는 가장 간단한 방법은 이미 정의된 다른 수정자 팩토리를 결합하는 맞춤 수정자 팩토리를 구현하는 것입니다. 더 많은 맞춤 동작이 필요한 경우 하위 수준이지만 더 많은 유연성을 제공하는 Modifier.Node API를 사용하여 수정자 요소를 구현합니다.
기존 수정자 연결
기존 수정자를 사용하여 맞춤 수정자를 만들 수 있는 경우가 많습니다. 예를
들어 Modifier.clip()은 graphicsLayer
수정자를 사용하여 구현됩니다. 이 전략은 기존 수정자 요소를 사용하며 자체 맞춤 수정자 팩토리를 제공합니다.
자체 맞춤 수정자를 구현하기 전에 동일한 전략을 사용할 수 있는지 확인하세요.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
또는 동일한 수정자 그룹을 자주 반복하는 경우 이를 자체 수정자로 래핑할 수 있습니다.
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
구성 가능한 수정자 팩토리를 사용하여 맞춤 수정자 만들기
구성 가능한 함수를 사용하여 맞춤 수정자를 만들어 기존 수정자에 값을 전달할 수도 있습니다. 이를 구성 가능한 수정자 팩토리라고 합니다.
구성 가능한 수정자 팩토리를 사용하여 수정자를 만들면
상위 수준의 Compose API(예: animate*AsState 및 기타 Compose
상태 지원 애니메이션 API)를 사용할 수도 있습니다. 예를 들어 다음 스니펫은 사용 설정/사용 중지 시 알파 변경에 애니메이션을 적용하는 수정자를 보여줍니다.
@Composable fun Modifier.fade(enable: Boolean): Modifier { val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f) return this then Modifier.graphicsLayer { this.alpha = alpha } }
맞춤 수정자가 CompositionLocal에서 기본값을 제공하는 편의 메서드인 경우 구성 가능한 수정자 팩토리를 사용하는 것이 가장 쉬운 구현 방법입니다.
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
이 접근 방식에는 몇 가지 주의사항이 있으며, 이는 다음 섹션에서 자세히 설명합니다.
CompositionLocal 값은 수정자 팩토리의 호출 사이트에서 확인됨
구성 가능한 수정자 팩토리를 사용하여 맞춤 수정자를 만들 때 컴포지션 로컬은 사용되는 컴포지션 트리가 아닌 생성되는 컴포지션 트리에서 값을 가져옵니다. 이로 인해 예기치 않은 결과가 발생할 수 있습니다. 예를 들어 이전에 언급한 컴포지션 로컬 수정자 예시를 구성 가능한 함수를 사용하여 약간 다르게 구현한다고 가정해 보겠습니다.
@Composable fun Modifier.myBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) } @Composable fun MyScreen() { CompositionLocalProvider(LocalContentColor provides Color.Green) { // Background modifier created with green background val backgroundModifier = Modifier.myBackground() // LocalContentColor updated to red CompositionLocalProvider(LocalContentColor provides Color.Red) { // Box will have green background, not red as expected. Box(modifier = backgroundModifier) } } }
수정자가 예상대로 작동하지 않는 경우 구성 가능한 로컬이 사용 사이트에서 올바르게 확인되고 안전하게 호이스팅될 수 있으므로 대신 맞춤 Modifier.Node를 사용하세요.
구성 가능한 함수 수정자는 건너뛸 수 없음
구성 가능한 팩토리 수정자는 반환 값이 있는 구성 가능한 함수를 건너뛸 수 없으므로 건너뛸 수 없습니다. 즉, 수정자 함수는 모든 리컴포지션에서 호출되며, 자주 리컴포지션되는 경우 비용이 많이 들 수 있습니다.
구성 가능한 함수 수정자는 구성 가능한 함수 내에서 호출해야 함
모든 구성 가능한 함수와 마찬가지로 구성 가능한 팩토리 수정자는 컴포지션 내에서 호출해야 합니다. 이렇게 하면 수정자를 호이스팅할 수 있는 위치가 제한됩니다. 컴포지션 외부로 호이스팅할 수 없기 때문입니다. 반면 구성 가능하지 않은 수정자 팩토리는 구성 가능한 함수 외부로 호이스팅하여 재사용을 더 쉽게 하고 성능을 개선할 수 있습니다.
val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations @Composable fun Modifier.composableModifier(): Modifier { val color = LocalContentColor.current.copy(alpha = 0.5f) return this then Modifier.background(color) } @Composable fun MyComposable() { val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher }
Modifier.Node를 사용하여 맞춤 수정자 동작 구현
Modifier.Node 는 Compose에서 수정자를 만들기 위한 하위 수준 API입니다. Compose가 자체 수정자를 구현하는 데 사용하는 것과 동일한 API이며 맞춤 수정자를 만드는 가장 성능이 좋은 방법입니다.
Modifier.Node를 사용하여 맞춤 수정자 구현
Modifier.Node를 사용하여 맞춤 수정자를 구현하는 데는 세 가지 부분이 있습니다.
- 수정자의 로직과
상태를 보유하는
Modifier.Node구현 - 수정자
노드 인스턴스를 만들고 업데이트하는
ModifierNodeElement - 이전에 설명한 대로 선택적 수정자 팩토리
ModifierNodeElement 클래스는 스테이트리스이며 각 리컴포지션에 새 인스턴스가 할당되는 반면 Modifier.Node 클래스는 상태 저장일 수 있으며 여러 리컴포지션에서 유지되고 재사용될 수도 있습니다.
다음 섹션에서는 각 부분을 설명하고 원을 그리는 맞춤 수정자를 빌드하는 예를 보여줍니다.
Modifier.Node
Modifier.Node 구현 (이 예에서는 CircleNode)은 맞춤 수정자의 기능을 구현합니다.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
이 예에서는 수정자 함수에 전달된 색상으로 원을 그립니다.
노드는 Modifier.Node와 0개 이상의 노드 유형을 구현합니다. 수정자에 필요한 기능에 따라 노드 유형이 다릅니다. 이전 예에서는 그릴 수 있어야 하므로 그리기 메서드를 재정의할 수 있는 DrawModifierNode를 구현합니다.
사용 가능한 유형은 다음과 같습니다.
노드 |
사용 정보 |
샘플 링크 |
래핑된 콘텐츠가 측정되고 배치되는 방식을 변경하는 |
||
레이아웃의 공간에 그리는 |
||
이 인터페이스를 구현하면 |
||
테스트, 접근성, 유사한 사용 사례에 사용할 수 있도록 시맨틱 키-값을 추가하는 |
||
상위 요소 레이아웃에 데이터를 제공하는 |
||
|
||
콘텐츠의 전역 위치가 변경되었을 수 있다면 레이아웃의 최종 |
||
|
||
작업을 다른 이는 여러 노드 구현을 하나로 구성하는 데 유용할 수 있습니다. |
||
|
해당 요소에서 업데이트가 호출되면 노드가 자동으로 무효화됩니다. 이 예는 DrawModifierNode이므로 요소에서 업데이트가 호출될 때마다 노드가 다시 그리기를 트리거하고 색상이 올바르게 업데이트됩니다. 노드 자동 무효화
선택 해제 섹션에 설명된 대로
자동 무효화를 선택 해제할 수 있습니다.
ModifierNodeElement
ModifierNodeElement는 맞춤 수정자를 만들거나 업데이트하기 위한 데이터를 보유하는 변경 불가능한 클래스입니다.
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
ModifierNodeElement 구현은 다음 메서드를 재정의해야 합니다.
create: 수정자 노드를 인스턴스화하는 함수입니다. 수정자가 처음 적용될 때 노드를 만들기 위해 호출됩니다. 일반적으로 이는 노드를 구성하고 수정자 팩토리에 전달된 매개변수로 노드를 구성하는 것과 같습니다.update: 이 수정자가 이 노드가 이미 있는 동일한 위치에 제공되지만 속성이 변경될 때마다 이 함수가 호출됩니다. 이는 클래스의equals메서드에 의해 결정됩니다. 이전에 생성된 수정자 노드는update호출에 매개변수로 전송됩니다. 이때 업데이트된 매개변수에 상응하도록 노드의 속성을 업데이트해야 합니다. 이러한 방식으로 노드를 재사용할 수 있는 기능은Modifier.Node가 제공하는 성능 향상의 핵심입니다. 따라서update메서드에서 새 노드를 만드는 대신 기존 노드를 업데이트해야 합니다. 원 예시에서는 노드의 색상이 업데이트됩니다.
또한 ModifierNodeElement 구현은 equals 및 hashCode도 구현해야 합니다. 이전 요소와의 equals 비교가 false를 반환하는 경우에만 update가 호출됩니다.
이전 예에서는 데이터 클래스를 사용하여 이를 달성합니다. 이러한 메서드는 노드를 업데이트해야 하는지 확인하는 데 사용됩니다. 요소에 노드를 업데이트해야 하는지 여부에 영향을 미치지 않는 속성이 있거나 바이너리 호환성상의 이유로
데이터 클래스를 피하려는 경우
equals 및 hashCode를 수동으로 구현할 수 있습니다. 예를 들어
패딩 수정자 요소가 있습니다.
수정자 팩토리
이는 수정자의 공개 API 노출 영역입니다. 대부분의 구현은 수정자 요소를 만들고 수정자 체인에 추가합니다.
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
전체 예
이 세 부분은 함께 Modifier.Node API를 사용하여 원을 그리는 맞춤 수정자를 만듭니다.
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) // ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } } // Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Modifier.Node를 사용하는 일반적인 상황
Modifier.Node를 사용하여 맞춤 수정자를 만들 때 발생할 수 있는 일반적인 상황은 다음과 같습니다.
매개변수 없음
수정자에 매개변수가 없으면 업데이트할 필요가 없으며 데이터 클래스일 필요도 없습니다. 다음은 구성 가능한 항목에 고정된 패딩 양을 적용하는 수정자의 샘플 구현입니다.
fun Modifier.fixedPadding() = this then FixedPaddingElement data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() { override fun create() = FixedPaddingNode() override fun update(node: FixedPaddingNode) {} } class FixedPaddingNode : LayoutModifierNode, Modifier.Node() { private val PADDING = 16.dp override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val paddingPx = PADDING.roundToPx() val horizontal = paddingPx * 2 val vertical = paddingPx * 2 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) val width = constraints.constrainWidth(placeable.width + horizontal) val height = constraints.constrainHeight(placeable.height + vertical) return layout(width, height) { placeable.place(paddingPx, paddingPx) } } }
참조 컴포지션 로컬
Modifier.Node 수정자는 CompositionLocal과 같은 Compose 상태 객체의 변경사항을 자동으로 관찰하지 않습니다. Modifier.Node 수정자가 구성 가능한 팩토리로만 생성된 수정자에 비해 갖는 장점은 currentValueOf를 사용하여 수정자가 할당된 위치가 아닌 UI 트리에서 수정자가 사용되는 위치에서 컴포지션 로컬의 값을 읽을 수 있다는 것입니다.
하지만 수정자 노드 인스턴스는 상태 변경을 자동으로 관찰하지 않습니다. 컴포지션 로컬 변경에 자동으로 반응하려면 범위 내에서 현재 값을 읽으면 됩니다.
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope&IntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
이 예에서는 LocalContentColor의 값을 관찰하여 색상을 기반으로 배경을 그립니다. ContentDrawScope는 스냅샷 변경사항을 관찰하므로 LocalContentColor의 값이 변경되면 자동으로 다시 그려집니다.
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
범위 외부의 상태 변경에 반응하고 수정자를 자동으로 업데이트하려면 ObserverModifierNode를 사용하세요.
예를 들어 Modifier.scrollable은 이 기법을 사용하여
LocalDensity의 변경사항을 관찰합니다. 간단한 예는 다음 예에 나와 있습니다.
class ScrollableNode : Modifier.Node(), ObserverModifierNode, CompositionLocalConsumerModifierNode { // Place holder fling behavior, we'll initialize it when the density is available. val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity)) override fun onAttach() { updateDefaultFlingBehavior() observeReads { currentValueOf(LocalDensity) } // monitor change in Density } override fun onObservedReadsChanged() { // if density changes, update the default fling behavior. updateDefaultFlingBehavior() } private fun updateDefaultFlingBehavior() { val density = currentValueOf(LocalDensity) defaultFlingBehavior.flingDecay = splineBasedDecay(density) } }
수정자에 애니메이션 적용
Modifier.Node 구현은 coroutineScope에 액세스할 수 있습니다. 이를 통해
Compose Animatable API를 사용할 수 있습니다. 예를 들어 이 스니펫은 이전에 표시된 CircleNode를 수정하여 반복적으로 페이드 인 및 페이드 아웃합니다.
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private lateinit var alpha: Animatable<Float, AnimationVector1D> override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { alpha = Animatable(1f) coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
위임을 사용하여 수정자 간에 상태 공유
Modifier.Node 수정자는 다른 노드에 위임할 수 있습니다. 이는 여러 수정자에서 공통 구현을 추출하는 것과 같은 여러 사용 사례가 있지만 수정자 간에 공통 상태를 공유하는 데에도 사용할 수 있습니다.
예를 들어 상호작용 데이터를 공유하는 클릭 가능한 수정자 노드의 기본 구현은 다음과 같습니다.
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
노드 자동 무효화 선택 해제
Modifier.Node 노드는 해당 ModifierNodeElement가 업데이트를 호출할 때 자동으로 무효화됩니다. 복잡한 수정자의 경우 수정자가 단계를 무효화하는 시점을 더 세밀하게 제어하기 위해 이 동작을 선택 해제할 수 있습니다.
이는 맞춤 수정자가 레이아웃과 그리기를 모두 수정하는 경우 특히 유용합니다. 자동 무효화를 선택 해제하면 color와 같은 그리기 관련 속성만 변경될 때 그리기를 무효화할 수 있습니다. 이렇게 하면 레이아웃이 무효화되지 않고 수정자의 성능을 개선할 수 있습니다.
이러한 가상 예는 속성으로 color, size, onClick 람다가 있는 수정자를 사용하여 다음 예에 나와 있습니다. 이 수정자는 필요한 것만 무효화하고 불필요한 무효화는 건너뜁니다.
class SampleInvalidatingNode( var color: Color, var size: IntSize, var onClick: () -> Unit ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode { override val shouldAutoInvalidate: Boolean get() = false private val clickableNode = delegate( ClickablePointerInputNode(onClick) ) fun update(color: Color, size: IntSize, onClick: () -> Unit) { if (this.color != color) { this.color = color // Only invalidate draw when color changes invalidateDraw() } if (this.size != size) { this.size = size // Only invalidate layout when size changes invalidateMeasurement() } // If only onClick changes, we don't need to invalidate anything clickableNode.update(onClick) } override fun ContentDrawScope.draw() { drawRect(color) } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val size = constraints.constrain(size) val placeable = measurable.measure(constraints) return layout(size.width, size.height) { placeable.place(0, 0) } } }