Compose App의 UI는 UI를 그리는 Composable이 작성된 Composable 함수를 호출하여 만들어진다.
Composable 함수 : @Composable 어노테이션을 포함하는 코틀린 함수.
@Composable : Composable 어노테이션은 Compose 컴파일러에게 해당 함수가 데이터를 UI 요소로 변환한다는 것을 알리는 역할을 한다. 따라서, Composable을 사용하여 UI를 그리기 위해 Composable 함수를 작성한다면, 반드시 어노테이션이 작성되어야 한다.
Composable 함수는 @Composable 어노테이션을 포함하고 코틀린 함수의 시그니처와 같은 구성요소를 가진다.
Composable 함수 명명규칙 → 명사 또는 형용사를 접두어로 갖는 명사
androidx.compose.material.Text()
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
val mergedStyle = style.merge(
TextStyle(
color = textColor,
fontSize = fontSize,
fontWeight = fontWeight,
textAlign = textAlign,
lineHeight = lineHeight,
fontFamily = fontFamily,
textDecoration = textDecoration,
fontStyle = fontStyle,
letterSpacing = letterSpacing
)
)
BasicText(
text,
modifier,
mergedStyle,
onTextLayout,
overflow,
softWrap,
maxLines,
)
Text()는 BasicText()를 호출하지만, Text()를 사용하는 이유는 테마의 스타일 정보를 사용하기 때문이다.
BasicText()
require(maxLines > 0) { "maxLines should be greater than 0" }
val selectionRegistrar = LocalSelectionRegistrar.current
val density = LocalDensity.current
val fontFamilyResolver = LocalFontFamilyResolver.current
val selectableId =
rememberSaveable(text, selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) {
selectionRegistrar?.nextSelectableId() ?: SelectionRegistrar.InvalidSelectableId
}
val controller = remember {
TextController(
TextState(
TextDelegate(
text = AnnotatedString(text),
style = style,
density = density,
softWrap = softWrap,
fontFamilyResolver = fontFamilyResolver,
overflow = overflow,
maxLines = maxLines,
),
selectableId
)
)
}
val state = controller.state
if (!currentComposer.inserting) {
controller.setTextDelegate(
updateTextDelegate(
current = state.textDelegate,
text = text,
style = style,
density = density,
softWrap = softWrap,
fontFamilyResolver = fontFamilyResolver,
overflow = overflow,
maxLines = maxLines,
)
)
}
state.onTextLayout = onTextLayout
controller.update(selectionRegistrar)
if (selectionRegistrar != null) {
state.selectionBackgroundColor = LocalTextSelectionColors.current.backgroundColor
}
Layout(modifier.then(controller.modifiers), controller.measurePolicy)
BasicText()는 최종적으로 Layout()이라는 컴포저블을 호출한다.
Layout()
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
},
skippableUpdate = materializerOf(modifier),
content = content
)
Layout()은 레이아웃을 위한 핵심 컴포저블 함수이고, 자식 요소의 크기와 위치를 지정한다.
ReusableComposeNode() : UI 요소인 Node를 내보낸다. (=Compose 내부 자료구조에 자식 Node를 추가하는 것)
ReusalbeComposeNode()
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startReusableNode()
if (currentComposer.inserting) {
currentComposer.createNode(factory)
} else {
currentComposer.useNode()
}
currentComposer.disableReusing()
Updater<T>(currentComposer).update()
currentComposer.enableReusing()
SkippableUpdater<T>(currentComposer).skippableUpdate()
currentComposer.startReplaceableGroup(0x7ab4aae9)
content()
currentComposer.endReplaceableGroup()
currentComposer.endNode()
ReusalbeComposeNode는 새로운 Node가 생성되어야 할지, 기존의 Node를 재사용할지 결정하는 역할을 한다.
그러고 나서 업데이트를 수행하고, 마지막으로 content()를 호출해 콘텐츠를 노드에 내보낸다.
currentComposer : androidx.compose.runtime.Composables에 있는 Composer타입의 최상위 변수
val currentComposer: Composers
@ReadOnlyComposable
@Composable get() { throw NotImplementedError("Implemented as an intrinsic") }
Layout()은 factory인자에 ComposeUiNode.constructor를 전달하므로, currentComposer.createNode(factory)에 ComposeUiNode 타입이 factory에 담겨 전달되며, 이를 통해 생성된 UI 요소를 나타내는 노드의 기능이 ComposeUiNode 인터페이스에 정의됨을 알 수 있다.
ComposeUiNode
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
var measurePolicy: MeasurePolicy
var layoutDirection: LayoutDirection
var density: Density
var modifier: Modifier
var viewConfiguration: ViewConfiguration
/**
* Object of pre-allocated lambdas used to make use with ComposeNode allocation-less.
*/
companion object {
val Constructor: () -> ComposeUiNode = LayoutNode.Constructor
val SetModifier: ComposeUiNode.(Modifier) -> Unit = { this.modifier = it }
val SetDensity: ComposeUiNode.(Density) -> Unit = { this.density = it }
val SetMeasurePolicy: ComposeUiNode.(MeasurePolicy) -> Unit = { this.measurePolicy = it }
val SetLayoutDirection: ComposeUiNode.(LayoutDirection) -> Unit = { this.layoutDirection = it }
val SetViewConfiguration: ComposeUiNode.(ViewConfiguration) -> Unit = { this.viewConfiguration = it }
}
Node
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
@Composable
@ReadOnlyComposable
fun stringResource(@StringRes id: Int, vararg formatArgs: Any): String {
val resources = resources()
return resources.getString(id, *formatArgs)
}
여러 개의 컴포저블 함수에서 한 개의 상태를 공유하고 싶을 수도 있다.
class ColorPickerDemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.width(min(400.dp, maxWidth)),
horizontalAlignment = Alignment.CenterHorizontally
) {
val color = remember { mutableStateOf(Color.Magenta) }
ColorPicker(color)
Text(
modifier = Modifier
.fillMaxWidth()
.background(color.value),
text = "#${color.value.toArgb().toUInt().toString(16)}",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h4.merge(
TextStyle(
color = color.value.complementary()
)
)
)
}
}
}
}
}
Color.complementary()
fun Color.complementary() = Color(
red = 1F - red,
green = 1F - green,
blue = 1F - blue
)
현재 color의 보색을 반환하는 함수.
ColorPicker()
@Composable
fun ColorPicker(color: MutableState<Color>) {
val red = color.value.red
val green = color.value.green
val blue = color.value.blue
Column {
Slider(
value = red,
onValueChange = { color.value = Color(it, green, blue) })
Slider(
value = green,
onValueChange = { color.value = Color(red, it, blue) })
Slider(
value = blue,
onValueChange = { color.value = Color(red, green, it) })
}
}
중요사항
setContent
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
setParentCompositionContext()
fun setParentCompositionContext(parent: CompositionContext?) {
parentContext = parent
}
parent를 생략할 경우 어떻게 되는지 ??
ComposeView는 setContentView와 setContent의 사이를 연결한다.
setContentView(
ComposeView(this).apply {
setContent {
MyComposableContent()
}
}
)
앞서 우리는 다음과 같이 몇 가지 변경자(modifier)들을 살펴보았다.
이 변경자들은 UI와 상응하는 너비와 크기를 제어한다.
@Composable
fun OrderDemo() {
var color by remember { mutableStateOf(Color.Blue) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp)
.border(BorderStroke(width = 2.dp, color = color))
.background(Color.LightGray)
.clickable {
color = if (color == Color.Blue)
Color.Red
else
Color.Blue
}
)
}
padding()
→ Android의 padding과 동일하며, padding 크기를 조정border()
→ 테두리 조정background()
→ 배경색을 제어clickable{ }
→ 사용자가 UI 요소를 클릭함으로써 컴포저블 함수와 상호작용할 수 있게 해준다.@Composable
fun TextWithYellowBackground(
text: String,
modifier: Modifier = Modifier
) {
Text(
text = text,
modifier = modifier.background(Color.Yellow)
)
}
컴포저블은 다음과 같이 modifier
라는 파라미터로 변경자 체이닝을 전달받을 수 있다.
background의 변경자 속성을 추가하여 Text 컴포저블을 정의하는 모습이다.
어떻게 .background()
를 통해서 변경자의 속성을 추가할 수 있을까?
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
}
)
)
background()의 내부 코드를 살펴보자.
Modifier
의 확장 함수로, then()
을 사용하여, 변경자를 서로 연결한다.
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
fun Modifier.drawYellowCross() = then(
object : DrawModifier {
override fun ContentDrawScope.draw() {
drawLine(
color = Color.Yellow,
start = Offset(0F, 0F),
end = Offset(size.width - 1, size.height - 1),
strokeWidth = 10F
)
drawLine(
color = Color.Yellow,
start = Offset(0F, size.height - 1),
end = Offset(size.width - 1, 0F),
strokeWidth = 10F
)
drawContent()
}
}
)
다음과 같이, 우리는 Modifier의 확장함수로 선언하여 커스텀 변경자를 구현할 수 있다.
DrawModifer의 인스턴스를 상속 받는다.
사용 예시)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// OrderDemo()
// TextWithYellowBackground(
// text = "Hello Compose",
// modifier = Modifier.padding(32.dp)
// )
Text(
text = "Hello Compose",
modifier = Modifier
.fillMaxSize()
.drawYellowCross(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h1
)
}
}