Jetpack Compose Modifier 심층 분석

Default Parameter

@Composable
fun PaddedColumn(modifier: Modifier = Modifier) {
    Column(modifier.padding(10.dp)) {
        // ...
    }
}

modifier: Modifier = Modifier와 같은 코드는 어떻게 만들어지는 것일까? 해당 식에서 처음으로 나타나는 Modifier는 타입이고 두 번째로 나타나는 Modifier는 객체이다.

Modifier Source Code를 확인해 보면 다음과 같다.

interface Modifier {
  fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
  fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
  fun any(predicate: (Element) -> Boolean): Boolean
  fun all(predicate: (Element) -> Boolean): Boolean
  infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)
  /* ... */
  companion object : Modifier {
    override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
    override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
    override fun any(predicate: (Element) -> Boolean): Boolean = false
    override fun all(predicate: (Element) -> Boolean): Boolean = true
    override infix fun then(other: Modifier): Modifier = other
    override fun toString() = "Modifier"
    }
}

Source Code에서 Modifierinterface이며 Modifier interface를 상속하는 companion object의 형태로 구현이 되어 있다. 이 방법을 통해 Modifier라는 이름으로 타입과 객체의 역할을 동시에 하고 있음을 알 수 있다.

Modifier의 순서

Modifier는 순서에 따라 그 결과가 달라지는 모습을 보인다. 이러한 특징이 UI를 만들기 위해 달성해야 하는 목표를 쉽게 달성할 수 있고 기존 Android View System보다 더 강력한 도구로서의 기능을 제공해준다.

Modifier의 순서를 부여하기 위해 Modifier.xxx()의 Source Code를 확인해 보면 다음과 같다.

fun Modifier.background(  
    color: Color,  
    shape: Shape = RectangleShape  
) = this.then(  
    Background(  
        color = color,  
        shape = shape,  
        inspectorInfo = debugInspectorInfo {  
            name = "background"  
            value = color  
            properties["color"] = color  
            properties["shape"] = shape  
        }  
    )  
)

위 코드의 Modifier.background(...)는 Composable에 배경을 지정해 준다. 위 코드에서 볼 수 있는 몇 가지의 특징은

  • 확장 함수로 구현됨
  • this.then
  • Background DrawModifier implementation 으로 볼 수 있을 것이다. 그 중 this.then의 코드를 확인해 보면
    // interface Modifier in Modifier.kt
    infix fun then(other: Modifier): Modifier =  
      if (other === Modifier) this else CombinedModifier(this, other)
    

    Modifier interface에 해당 함수가 정의되어 있다. otherModifier 기본 객체이면 현재 Modifier를 내보내고 그렇지 않을 경우 현재 Modifier와 다른 Modifier를 조합하는 CombinedModifier를 반환한다.

CombinedModifier의 정의는 다음과 같다.

class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
) : Modifier {
    override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
        inner.foldIn(outer.foldIn(initial, operation), operation)

    override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
        outer.foldOut(inner.foldOut(initial, operation), operation)

    override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.any(predicate) || inner.any(predicate)

    override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.all(predicate) && inner.all(predicate)

    override fun equals(other: Any?): Boolean =
        other is CombinedModifier && outer == other.outer && inner == other.inner

    override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()

    override fun toString() = "[" + foldIn("") { acc, element ->
        if (acc.isEmpty()) element.toString() else "$acc, $element"
    } + "]"
}

CombinedModifierouter, inner Modifier 정보를 유지하고 foldInfoldOut을 이용하여 정해진 순서를 유지한다.

Modifier의 범위

Row {
    Text(
        text = "Test",
        modifier = Modifier.align(Alignment.CenterVertically)
    )
}
Column {
    Text(
        text = "Test",
        modifier = Modifier.align(Alignment.CenterHorizontally)
    )
}

Modifier.align은 같은 기능을 하는 것처럼 보이지만 CenterVerticallyCenterHorizontally를 서로 바꾸면 컴파일되지 않는다. Modifier.alignRowScope, ColumnScope interface에 각각 다른 형태로 정의되어 있다.

interface RowScope {
    fun Modifier.align(alignment: Alignment.Vertical): Modifier
}
interface ColumnScope {
    fun Modifier.align(alignment: Alignment.Horizontal): Modifier
}

그리고 Row Composable 함수는 다음과 같이 정의되어 있다.

@Composable  
inline fun Row(  
    modifier: Modifier = Modifier,  
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,  
    verticalAlignment: Alignment.Vertical = Alignment.Top,  
    content: @Composable RowScope.() -> Unit  
)
@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
)

content: @Composable RowScope.() -> Unit, content: @Composable ColumnScope.() -> Unit을 통해 Lambda Composable(하위 컴포저블 호출에 사용)이 각각 RowScope, ColumnScope 위에서 동작하도록 만든다.

Modifier Implementation

Modifier를 구현하기 위해서는 Modifier.Element또는 Modifier.Element에 일부 기능을 미리 추가해 정의된 인터페이스(ex. DrawModifier)와 InspectorValueInfo 추상 클래스를 상속하여 만든다.

Predefined interfaces

Modifier.Element 외에 기능을 미리 추가하여 정의해 놓은 interface가 존재한다.

  • DrawModifier: Composable의 surface에 그림을 그릴 수 있는 함수를 추가한 Modifier
  • LayoutModifier: size manipulation을 위한 함수를 추가한 Modifier

© 2021. All rights reserved.

Powered by Hydejack v9.1.6