make a splash
728x90

컴포저블 함수 자세히 알아보기

컴포저블 함수의 구성요소

  • [선택사항] 가시성 변경자 - private, protected, internal, public)
  • fun 키워드
  • 함수명
    • 파스칼 표기법 사용 : 대문자로 시작, 나머지는 소문자. 여러단어일 경우 명사, 명사구 사용.
  • 매개변수 목록
  • [선택사항] 반환타입
    • 아무것도 작성하지않으면 Unit 반환
    • Unit : Unit이라는 하나의 값만 갖는 타입
    • 코틀린은 void 키워드를 알지 못함 → 함수는 무언가를 반환해야 함.
    • 반환타입을 생략해서 반환타입이 kotlin.Unit임을 암시적으로 제시
  • 코드 블록
    • 하나의 표현식만 실행돼야 하는 경우, 축약어로 사용
    • @Composable fun ShortText( text: String = "" ) = Text(text = text)

UI요소 내보내기 emit

androidx.compose.meterial.Text()가 호출됐을 때 일어나는 일

  • Text()
    • textColor과 mergedStyle을 정의
    • BasicText()에 변수를 전달
    • BasicText()를 사용할 수도 있지만, Text()는 테마의 스타일 정보를 사용하기 때문에 Text() 선택.
  • @Composable fun Text( // 매개변수 목록 ) { val textColor = color.takeOrElse { style.color.takeOrElse { LocalContentColor.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, inlineContent ) }
  • BasicText()
    • BasicText()는 즉시 CoreText()에 위임 (TextController 로 바뀐건가?)
    • CoreText()는 내부 컴포저블 함수로, 앱에서는 사용할 수 없음.
    • Layout() 호출
  • @OptIn(InternalFoundationTextApi::class) @Composable fun BasicText( // Text()에서 받은 매개변수 ) { require(maxLines > 0) { "maxLines should be greater than 0" } // selection registrar, if no SelectionContainer is added ambient value will be null val selectionRegistrar = LocalSelectionRegistrar.current val density = LocalDensity.current val fontFamilyResolver = LocalFontFamilyResolver.current val selectionBackgroundColor = LocalTextSelectionColors.current.backgroundColor val (placeholders, inlineComposables) = resolveInlineContent(text, inlineContent) val selectableId = if (selectionRegistrar == null) { SelectionRegistrar.InvalidSelectableId } else { rememberSaveable(text, selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) { selectionRegistrar.nextSelectableId() } } val controller = remember { TextController( TextState( TextDelegate( text = text, style = style, density = density, softWrap = softWrap, fontFamilyResolver = fontFamilyResolver, overflow = overflow, maxLines = maxLines, placeholders = placeholders ), 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, placeholders = placeholders, ) ) } state.onTextLayout = onTextLayout state.selectionBackgroundColor = selectionBackgroundColor controller.update(selectionRegistrar) Layout( content = if (inlineComposables.isEmpty()) { {} } else { { InlineChildren(text, inlineComposables) } }, modifier = modifier.then(controller.modifiers), measurePolicy = controller.measurePolicy ) }
  • Layout
    • androidx.compose.ui.layout 패키지에 포함
    • 레이아웃을 위한 핵심 컴포저블 함수
    • 자식 요소의 크기와 위치를 지정함
    • ReusableComposeNode() 를 호출 : 노드(UI 요소 계층 구조)를 내보냄
    • 전달 매개변수
      • factory : 팩토리를 통해 노드가 생성되는데, 이 factory 인자를 통해 팩토리 전달
      • update : 노드에서 업데이트를 수행하는 코드를 전달받음
      • skippableUpdate : 변경자를 조작하는 코드를 전달받음
      • content : 자식노드가 되는 컴포저블 함수를 포함함
  • @Suppress("ComposableLambdaParameterPosition") @UiComposable @Composable inline fun Layout( content: @Composable @UiComposable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ) { 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 ) }
  • ReusableComposeNode()
    • currentComposer : `package androidx.compose.runtime.Composables.kt`에 있는 최상위 변수. Composer 타입으로, 인터페이스다.
    • 새로운 노드를 생성할지, 기존 노드를 재사용할지 결정함.
    • 결정 후, 업데이트를 수행함.
    • content()를 호출하여 콘텐츠를 노드에 내보냄.
  • @Composable @ExplicitGroupsComposable inline fun <T, reified E : Applier<*>> ReusableComposeNode( noinline factory: () -> T, update: @DisallowComposableCalls Updater<T>.() -> Unit, noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit, content: @Composable () -> 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() SkippableUpdater<T>(currentComposer).skippableUpdate() currentComposer.startReplaceableGroup(0x7ab4aae9) content() currentComposer.endReplaceableGroup() currentComposer.endNode() }
  • ReusableComposeNode() 에서 factory 인자노드를 사용할 때 ComposeUiNode.Constructor를 사용
  • → 노드의 기능은 ComposeUiNode 인터페이스에 정의된다.
  • ComposeUiNode.Constructor를 전달
  • ComposeUiNode
    • 노드의 프로퍼티 (책에서는 4개라는데, 내가 보니까 5개였음)
      • MeasurePolicy
      • LayoutDirection
      • Density
      • Modifier
      • ViewConfiguration ( ← 책에 없는 부분)
  • @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 } } }

값 반환

앞서 살펴본 것 처럼 컴포저블 함수는 UI 요소나 계층 구조를 내보내는 일을 수행함.

그럼 언제 Unit이 아닌 다른 값을 반환해야 할까?

  • stringResource()를 호출하는 과정
Text(text = stringResource(id = R.stirng.문자열이름))
  • stringResource()
    • resource() 도 컴포저블 함수다.
    • @Composable @ReadOnlyComposable internal fun resources(): Resources { LocalConfiguration.current return LocalContext.current.resources }
    • LocalContext.current.resources 를 반환함.
    • LocalContext : AndroidCompositioinLocals.android.kt 파일에 정의된 최상위 변수. val LocalContext = staticCompositionLocalOf{ noLocalProvidedFor("LocalContext") }
    • StaticProvidableCompositionLocal 의 인스턴스를 반환 → 리소스에 접근할 수 있게 해줌.
  • @Composable @ReadOnlyComposable fun stringResource(@StringRes id: Int): String { val resources = resources() return resources.getString(id) }

<정리>

반환된 데이터가 잿팩 컴포즈와 아무런 관련이 없을지라도 이 데이터를 받는 코드는 반드시 잿팩 컴포즈 매커니즘을 따라야 한다.

결국 이 데이터는 컴포저블 함수에서 호출 된 것이므로.

구정이나 재구성의 일부인 무언가를 반환해야 한다면, 그 함수는 반드시 @Composable 어노테이션을 포함한 컴포저블 함수로 만들어야 한다.

이러한 컴포저블 함수는 카멜 표기법을 따름


UI구성과 재구성

컴포저블 함수 간 상태 공유

// 세 개의 슬라이더를 수직으로 그룹화
@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)})
    }
}
  • Slider
    • value : 슬라이더가 나타낼 값 명시
    • onValueChange : 슬라이더를 드래그하거나 밑의 선을 클릭할 시 호출.
    • color.value = Color(it, green, blue) : 새 Color 객체 생성 → color.value에 값 할당. 다른 슬라이더에 의해 제어되는 색상은 지역변수에서 가져옴.

fun ColorPicker(color: Color?) 형태로 직접 전달하면 안될까?

피커로 설정한 색 → 텍스트 배경색

피커로 설정한 색의 보색 → 텍스트의 색상

ColorPicker()는 색상을 내보내지 않음.

ColorPicker() 내부에서 색상변경이 일어나므로 호출자에게 이런 변경사항을 알려줘야함.

하지만 매개변수로 전달되는 Color 인스턴스는 이러한 역할을 수행하지 못함 (코틀린 함수 매개변수가 변경 불가능한 값이기 때문)

전역 프로퍼티로 구현하면 안될까?

젯팩 컴포즈에서는 전역 프로퍼티 권장 X

컴포저블은 전역 변수를 절대 사용해서 X

컴포저블 함수의 모습과 행위에 영향을 주는 모든 데이터는 매개변수로 전달하자!

데이터가 함수 내부에서 변경된다면 MutableState를 사용하여 상태를 전달하고, 컴포저블함수는 상태를 전달받아 컴포저블을 호출한 곳으로 상태를 옮긴다. (상태 호이스팅 state hoisting)

  • 텍스트
    • Modifier.width(min(400.dp, maxWidth)) : 작은 값 선택. maxWidth는 BoxWithConstraints() 컴포저블에 의해 정의됨.
    • val color = remember { mutableStateOf(Color.Magenta) } : 상태는 기억되고 color에 할당된다
    • text color와 background color 를 상태(color)를 전달하는 대신 color.value를 전달 ⇒ 변경자라고 함.
    • remember
    • 재구성은 MutableStateOf에 대한 참조를 나타내는 mutableStateOf에서 생성된 값을 전달받는 생상으로 이어진다.
    • remember로 전달된 람다 표현식은 연산이라고 함.
    • 이 연산은 한번만 실행됨.
  • 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 = color) Text( modifier = Modifier .fillMaxWidth() .background(color.value), text = "#${color.value.toArgb().toUInt().toString(16)}", textAlign = TextAlign.Center, style = MaterialTheme.typography.headlineMedium.merge( TextStyle( color = color.value.complementary(), ), ), ) } }

크기 제어

BoxWithConstraints()가 접근할 수 있는 요소

  • constraints
  • minWidth
  • minHeight
  • maxWidth
  • maxHeight

액티비티 내에서 컴포저블 계층 구조 나타내기

  • MainActivity 기본 구조
    • androidx.activity.ComponentActivity의 확장함수인 setContent()를 통해 계층구조를 액티비티에 임베디드함.
    • 액티비티에 있는 setContent를 호출할 수 없고, ComponentActivity를 확장한 액티비티에서 호출할 수 있다
    • androidx.appcompat.app.AppCompatActivity가 여기에 해당
    • AppCompatActivity 클래스는 툴바와 옵션 메뉴를 지원하는 것과 같은 이전 뷰 기반과 관련된 많은 기능을 상속하고 있음. 하지만 젯팩 컴포즈는 이러한 것을 다르게 처리한다.
    • 가능하다면 AppCompatActivity 사용을 피하고 ComponentActivity를 확장한다.
  • class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ... } }
  • setContent()
    • 매개변수
      • parent : null 이 가능한 CompositionContext
      • content : 선언하는 UI를 위한 컴포저블 함수
    • findViewById() : 액티비티가 이미 ComposeView의 인스턴스를 포함하는지 알아내기 위해 사용ComposeView를 반하지 않으면 새로운 인스턴스가 생성돼 setCcontentView에 전달 → setParentCompositionContext(), setContent() 호출
      • setParentCompositionContext()
        • AbstractComposeView에 포함.
        • 뷰 구성 시 부모가 되는 CompositionContext를 설정.
        • 컨텍스트가 null일 경우 부모가 자동으로 결정
      • abstract class AbstractComposeView @JvmOverloads constructor( // ... ) : ViewGroup(context, attrs, defStyleAttr) { // ... fun setParentCompositionContext(parent: CompositionContext?) { parentContext = parent } // ... }
    • 포함된다면 해당 뷰의 setParentCompositionContext(), 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) } }

컴포저블 함수의 행위 수정

컴포저블의 시각적 형태나 행위는 매개변수나 변경자 또는 두가지 모두를 통해 제어할 수 있다

변경자들은 행동draggable(), 정렬alignByBaseline(), 그리기paint()와 같은 여러 범주 중 하나에 할당 될 수 있다.

@Composable
fun OrderDemo() {
    var color by remember = { mutableStateOf(Color.Blue) }
    Box(
        modifier = Modifier
                                .fillMaxSize()
                                .padding(32.dp)
                                .border(BorderStroke(width = 2.dp, color = color))
                                .clikable {
                                    color = if (color = Color.Blue) Color.Red else Color.Blue
                                }
    )
}

. 을 사용해 여러 변경자를 결합함으로써 변경자 체이닝을 정의한다.

변경자가 사용되는 순서를 명시할 수 있다.

clikable { … } 컴포저블 영역 내부를 클릭했을 때만 반응. clikable { … } 이전에 일어나는 패딩에 대해서는 클릭 동작이 고려되지 않음.

변경자 동작 이해

modifier 매개변수로 변경자를 전달받아야 하며 Modifier 기본값을 할당해야 한다.

  • modifier 매개변수를 전달받는 방법
  • @Composable fun TextWithYellowBackgroud( text: String, modifier: Modifier = Modifier ) { Text( text = text, modifier = modifier.background(Color.Yellow) ) }
  • Modifier
    • Modifier은 인터페이스면서 동반객체.
    • androidx.compose.ui 에 포함.
    • then() : 두 변경자를 서로 연결함. 변경자에서 호출해야함.
    • Element
      • Element 인터페이스는 Modifier를 확장
      • Modifier 체인에 포함되는 단일 요소를 정의함.
    • @JvmDefaultWithCompatibility interface Element : Modifier { override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = operation(initial, this) override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = operation(this, initial) override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this) override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this) }
  • 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" }
  • modifier.background()
    • Modifier의 확장함수
    • then()
      • 호출하고 그 결과를 반환함.
      • other는 매개변수만 전달 받음.override infix fun then(other: Modifier): Modifier = other
      • background의 경우 other은 Background의 인스턴스가 된다.
      • other는 한개의 매개변수만 전달받으며, other 변경자는 현재 변경자와 연결되어야 한다.
  • 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 } ) )

커스텀 변경자 구현

Text(
    text = "Hello Compose",
    modifier = Modifier
        .fillMaxSize()
        .drawYellowCross(),
    textAlign = TextAlign.Center,
    style = MaterialTheme.typography.headlineLarge
)
  • Modifier 확장함수 drawYellowCross()
    • then()은 DrawModifier의 인스턴스를 전달받음.
    • ContentDrawScope의 확장함수인 draw()를 구현
    • drawContent() : UI 요소를 그리므로 함수가 언제 호출되느냐에 따라 해당 요소가 다른 그리기 기본 요소의 앞이나 뒤에 나타나게 됨. 지금은 마지막에 위치하므로 UI요소가 맨 위에 위치하게 됨.
  • 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() } } )
728x90
반응형
profile

make a splash

@vision333

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!