val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current
}
}
// NOTE(text-perf-review): It might be worthwhile writing a bespoke merge implementation that
// will avoid reallocating if all of the options here are the defaults
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,
)
BasicText에서 찾은 CoreText 관련 주석
The ID used to identify this CoreText. If this CoreText is removed from the composition tree and then added back, this ID should stay the same.
Notice that we need to update selectable ID when the input text or selectionRegistrar has been updated.
When text is updated, the selection on this CoreText becomes invalid. It can be treated as a brand new CoreText.
When SelectionRegistrar is updated, CoreText have to request a new ID to avoid ID collision.
@Suppress("NOTHING_TO_INLINE")
@Composable
@UiComposable
inline fun Layout(
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
val materialized = currentComposer.materialize(modifier)
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
set(materialized, ComposeUiNode.SetModifier)
},
)
}
ReusableComposeNode()
를 호출한다.update
: 업데이트를 수행하는 코드 작성skippableUpdate
: 변경자를 조작하는 코드 작성 -> materialized
로 바뀜/**
* Emits a recyclable node into the composition of type [T].
*
* This function will throw a runtime exception if [E] is not a subtype of the applier of the
* [currentComposer].
*
* @sample androidx.compose.runtime.samples.CustomTreeComposition
*
* @param factory A function which will create a new instance of [T]. This function is NOT
* guaranteed to be called in place.
* @param update A function to perform updates on the node. This will run every time emit is
* executed. This function is called in place and will be inlined.
*
* @see Updater
* @see Applier
* @see Composition
*/
// ComposeNode is a special case of readonly composable and handles creating its own groups, so
// it is okay to use.
@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ReusableComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
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()
currentComposer.endNode()
}
ReusableComposeNode
는 새로운 노드가 생성돼야 할지 또는 기존 노드를 재사용해야 할지를 결정한다.currentComposer
는 androidx.compose.runtime.Composables.kt에 있는 최상위 Composer 변수/**
* Interface extracted from LayoutNode to not mark the whole LayoutNode class as @PublishedApi.
*/
@PublishedApi
internal interface ComposeUiNode {
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 VirtualConstructor: () -> ComposeUiNode = { LayoutNode(isVirtual = true) }
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 }
}
}
@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) })
}
}
ColorPicker()
함수가 Color
가 아닌 MutableState<Color>
를 파라미터로 받는 이유
전역 프로퍼티를 사용하는 방식도 있겠지만 이는 권고하지 않는 방식이며, 되도록 Composable 함수의 모습과 행위에 영향을 주는 모든 데이터는 매개변수(parameter)로 전달하는 것이 좋다.
state hoisting(상태 호이스팅): 상태를 전달받아 Composable 함수를 호출한 곳으로 상태를 옮기는 것
중요 사항
Composable을 SideEffect가 없게 만들자. (동일한 인자로 함수를 반복적으로 호출해도 항상 동일한 결과를 생산함)
호출자로부터 모든 관련 데이터를 얻는 것 이외에도 전역프로퍼티에 의존하거나 예측 불가능한 값을 반환하는 함수 호출하는 것도 금지된다. -> 보통 IDE에서 경고해주는 것 같다.
fillMaxSize()
, fillMAxWidth()
, BoxWidthConstraints()
등을 잘 활용해보기setContent
parent: null 값이 가능한 CompositionContext
content: 선언하는 UI를 위한 Composable function
// ComponentActivity.kt
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
// 액티비티가 이미 ComposeView의 인스턴스를 포함하는지 알아내기 위해 사용된다.
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 { // findViewById 실패 시 새로운 인스턴스 생성: ComposeView
// 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)
}
}
// ComposeView.android.kt AbstractComposeView
private var parentContext: CompositionContext? = null
set(value) {
if (field !== value) {
field = value
if (value != null) {
cachedViewTreeCompositionContext = null
}
val old = composition
if (old !== null) {
old.dispose()
composition = null
// Recreate the composition now if we are attached.
if (isAttachedToWindow) {
ensureCompositionCreated()
}
}
}
}
// ComposeView.android.kt
// parentContext 대체재 찾기
private fun resolveParentCompositionContext() = parentContext
?: findViewTreeCompositionContext()?.cacheIfAlive()
?: cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive }
?: windowRecomposer.cacheIfAlive()
@Suppress("DEPRECATION") // Still using ViewGroup.setContent for now
private fun ensureCompositionCreated() {
if (composition == null) {
try {
creatingComposition = true
composition = setContent(resolveParentCompositionContext()) {
Content()
}
} finally {
creatingComposition = false
}
}
}
컴포넌트의 프로퍼티와 달리 Modifier는 전적으로 개발자의 판단에 따라 사용될 수 있다.
Modifier는 행동이나 정렬, 그리기와 같은 여러 범주 중 하나에 할당될 수 있다. Modifier 목록 링크
Modifier Chaining: 빌더 패턴처럼 Modifier의 속성을 정의
Modifier를 올바르게 사용하지 않으면, IDE에서는 다음과 같은 안내를 해준다.
어떤 경우에 이런 경고가 나오는 지 예시 코드를 보자
@Composable
fun TextWithWarning1(
name: String = "Default",
modifier: Modifier = Modifier,
callback: () -> Unit
) {
Text(text = "TextWithWarning1 $name!", modifier = modifier
.background(Color.Yellow)
.clickable { callback.invoke() })
}
@Composable
fun TextWithWarning2(test: Modifier = Modifier, name: String = "", callback: () -> Unit) {
Text(text = "TextWithWarning2 $name!", modifier = test
.background(Color.Yellow)
.clickable { callback.invoke() })
}
@Composable
fun TextWithoutWarning(
modifier: Modifier = Modifier,
buttonModifier: Modifier,
name: String = "",
callback: () -> Unit
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "TextWithoutWarning $name!", modifier = modifier
.padding(10.dp) // margin concept
.background(Color.Yellow)
.padding(10.dp) // real padding
.clickable { callback.invoke() })
val context = LocalContext.current
Button(
modifier = buttonModifier.clickable {
Toast.makeText(context, "버튼에 clickable을 넣으면?", Toast.LENGTH_SHORT).show()
},
onClick = { Toast.makeText(context, "버튼 클릭됨", Toast.LENGTH_SHORT).show() }) {
Text("버튼")
}
}
}
// Background.kt
fun Modifier.background(
color: Color,
shape: Shape = RectangleShape
) = this.then( // other parameter에 Background 인스턴스가 들어감
Background(
color = color,
shape = shape,
inspectorInfo = debugInspectorInfo {
name = "background"
value = color
properties["color"] = color
properties["shape"] = shape
}
)
)
// Modifier.kt
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
// Background.kt
private class Background constructor(
private val color: Color? = null,
private val brush: Brush? = null,
private val alpha: Float = 1.0f,
private val shape: Shape,
inspectorInfo: InspectorInfo.() -> Unit
) : DrawModifier, InspectorValueInfo(inspectorInfo) {
// ...
}
// DrawModifier.kt
@JvmDefaultWithCompatibility
interface DrawModifier : Modifier.Element {
fun ContentDrawScope.draw()
}
fun Modifier.drawWhiteCross() = then(
object : DrawModifier {
override fun ContentDrawScope.draw() {
drawLine(
color = Color.White,
start = Offset(0F, 0F),
end = Offset(size.width - 1, size.height - 1),
strokeWidth = 10F
)
drawLine(
color = Color.White,
start = Offset(0F, size.height - 1),
end = Offset(size.width - 1, 0F),
strokeWidth = 10F
)
drawContent()
}
}
)
fun Modifier.drawHiddenCross() = then(
object : DrawModifier {
override fun ContentDrawScope.draw() {
drawContent()
drawBehind {
drawLine(
color = Color.Blue,
start = Offset(0F, 0F),
end = Offset(size.width - 1, size.height - 1),
strokeWidth = 10F
)
drawLine(
color = Color.Blue,
start = Offset(0F, size.height - 1),
end = Offset(size.width - 1, 0F),
strokeWidth = 10F
)
}
}
}
)