Skip to content

Latest commit

Β 

History

History
137 lines (106 loc) Β· 5.42 KB

File metadata and controls

137 lines (106 loc) Β· 5.42 KB

composed vs Modifier.Node(+codegen) β€” before/after

두 μΌ€μ΄μŠ€λ‘œ λΉ„κ΅ν•œλ‹€. μ†ŒμŠ€: src/main/kotlin/.../compare/.

  • Case A β€” accentOverlay: CompositionLocal ν•˜λ‚˜ μ½μ–΄μ„œ draw. (μ „ν˜•μ  composed μ‚¬μš©)
  • Case B β€” pressScale: InteractionSource ꡬ독 + μ• λ‹ˆλ©”μ΄μ…˜. (무거운 composed μ‚¬μš©)

μ†μœΌλ‘œ μ“΄ μ½”λ“œ 쀄 수만 μ„Όλ‹€(주석 μ œμ™Έ, λŒ€λž΅μΉ˜). 생성 μ½”λ“œλŠ” 0으둜 μΉœλ‹€.

composed Node(손) codegen 없이 Node(손) 생성물
Case A ~7 ~10 ~55 ~46
Case B ~10 ~35 ~85 ~53

Case A β€” accentOverlay

BEFORE

fun Modifier.accentOverlayComposed(alpha: Float): Modifier = composed {
    val color = LocalAccentColor.current
    drawWithContent {
        drawContent()
        drawRect(color.copy(alpha = alpha))
    }
}

μˆ¨μ€ λΉ„μš©: λ°˜ν™˜ Modifier equals λΆˆμ•ˆμ • β†’ λΆ€λͺ¨ μž¬κ΅¬μ„± μ‹œ ν•˜μœ„ μž¬κ΅¬μ„±, composed λžŒλ‹€ 맀번 re-materialize, composition λ°– μ‚¬μš© λΆˆκ°€, layout inspector 에 이름 μ•ˆ 뜸.

AFTER

@ModifierNodeFactory(name = "accentOverlay")
internal class AccentOverlayNode(
    @Invalidates(Draw) var alpha: Float,
) : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val color = currentValueOf(LocalAccentColor)   // draw μ—μ„œ 읽으면 관찰됨 β†’ 둜컬 λ³€κ²½ μ‹œ μžλ™ redraw
        drawContent()
        drawRect(color.copy(alpha = alpha))
    }
}

생성: fun Modifier.accentOverlay(alpha) + AccentOverlayElement(create/update/equals/hashCode/inspector).

νŒμ • A β€” λͺ…ν™•ν•œ 이득

  • 손 μ½”λ“œ 7 β†’ 10쀄. λŠ˜μ–΄λ‚œ 3쀄은 μ „λΆ€ "μ‹€μ œ 정보"(무슨 λ…Έλ“œμΈμ§€ μ„ μ–Έ + var μ„ μ–Έ).
  • codegen μ—†μœΌλ©΄ 7 β†’ ~55쀄. 이 55쀄이 μ‹«μ–΄μ„œ λ‹€λ“€ composed 에 λ¨Έλ¬Έλ‹€. codegen 이 μ •ν™•νžˆ κ·Έ 벽을 μ—†μ•€λ‹€.
  • 덀: μ•ˆμ • equals, inspector 이름, composition λ°– μ‚¬μš© κ°€λŠ₯, @Invalidates(Draw) ↔ DrawModifierNode μ •ν•©μ„± 컴파일 체크.

Case B β€” pressScale

BEFORE

fun Modifier.pressScaleComposed(
    interactionSource: InteractionSource,
    pressedScale: Float,
): Modifier = composed {
    val pressed by interactionSource.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) pressedScale else 1f, label = "pressScale")
    graphicsLayer { scaleX = scale; scaleY = scale }
}

composed κ°€ 곡짜둜 ν•΄μ£ΌλŠ” 것: ꡬ독 lifecycle, μ• λ‹ˆλ©”μ΄μ…˜ μƒνƒœ remember, interactionSource ν‚€κ°€ λ°”λ€Œλ©΄ μž¬κ΅¬λ….

AFTER

@ModifierNodeFactory(name = "pressScale")
internal class PressScaleNode(
    @Invalidates(Draw) var pressedScale: Float,
    @Invalidates(None) @OnChange var interactionSource: InteractionSource,
) : Modifier.Node(), DrawModifierNode {

    private val scaleAnim = Animatable(1f)
    private var collectJob: Job? = null

    override fun onAttach() = subscribe()
    override fun onDetach() { collectJob?.cancel(); collectJob = null }
    fun onInteractionSourceChanged() = subscribe()   // μƒμ„±λœ update() κ°€ 호좜

    private fun subscribe() {
        collectJob?.cancel()
        collectJob = coroutineScope.launch {
            val presses = ArrayList<PressInteraction.Press>()
            interactionSource.interactions.collect { i ->
                when (i) {
                    is PressInteraction.Press   -> presses.add(i)
                    is PressInteraction.Release -> presses.remove(i.press)
                    is PressInteraction.Cancel  -> presses.remove(i.press)
                }
                launch { scaleAnim.animateTo(if (presses.isNotEmpty()) pressedScale else 1f) }
            }
        }
    }

    override fun ContentDrawScope.draw() {
        val s = scaleAnim.value        // snapshot μƒνƒœ β†’ ν”„λ ˆμž„λ§ˆλ‹€ μžλ™ redraw
        scale(s, s, center) { this@draw.drawContent() }
    }
}

νŒμ • B β€” codegen + @OnChange 둜 격차 μΆ•μ†Œ

  • 손 μ½”λ“œ 10 β†’ ~35쀄. Element(~50쀄) + μž¬κ΅¬λ… dedup(boundSource ν•„λ“œ, draw() 의 rebind()) 이 μ‚¬λΌμ‘Œλ‹€. 남은 μž₯황함은 Animatable / press μΉ΄μš΄νŒ… β€” μ§„μ§œ λ…Έλ“œ 행동 μ½”λ“œλ‹€.
  • @OnChange var interactionSource β†’ μƒμ„±λœ update() κ°€ ν•„λ“œ κ°±μ‹  ν›„ onInteractionSourceChanged() λ₯Ό ν˜ΈμΆœν•œλ‹€. composed 의 remember(key) μž¬κ΅¬λ…μ— λŒ€μ‘.
  • Animatable / press μΉ΄μš΄νŒ… λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈλŠ” μ—¬μ „νžˆ codegen λ²”μœ„ λ°– (λ…Έλ“œ μœ ν‹Έμ΄ 있으면 더 μ€„κ² μ§€λ§Œ λ³„κ°œ).

μ’…ν•©

λͺ¨λ””νŒŒμ΄μ–΄ λΆ€λ₯˜ codegen 효과
κ°’(νŒŒλΌλ―Έν„°/CompositionLocal) 읽고 draw/measure μ—μ„œ λ°˜μ‘ β€” μ»€μŠ€ν…€ λͺ¨λ””νŒŒμ΄μ–΄μ˜ λ‹€μˆ˜ 결정적. 7β†’10 vs 7β†’55. composed λ₯Ό λ²—μ–΄λ‚˜κ²Œ ν•΄μ€Œ
interaction / coroutine / animation @OnChange 둜 μž¬κ΅¬λ… 격차 μΆ•μ†Œ(10β†’35). 남은 μž₯황함(Animatable/μΉ΄μš΄νŒ…)은 μ§„μ§œ λ…Έλ“œ μ½”λ“œ
λͺ¨λ“  λΆ€λ₯˜ 곡톡 μ•ˆμ • equalsΒ·inspectorΒ·@Invalidates μ •ν•©μ„± 체크·ABI ν‘œλ©΄ μΆ•μ†ŒΒ·@SkipWhen* κ°€λ“œ
  • κ°’ 읽기 λΆ€λ₯˜: 결정적. fadingEdge(composition-free 체인)λŠ” composed μΌ€μ΄μŠ€κ°€ μ•„λ‹ˆμ—ˆκ³  raw λ…Έλ“œλ‘œ λ‚΄λ €μ„œ 손해 β€” ../ARCHITECTURE.md Β§7.
  • interaction λΆ€λ₯˜: @OnChange 이후 κ·Όμ ‘(κ°’ 읽기만큼 결정적이진 μ•ŠμŒ).
  • νŒŒλΌλ―Έν„° λ‹¨μœ„ invalidation 이 μ‹€μ œλ‘œ μž‘λ™ν•˜λŠ”μ§€(그리고 μ„±λŠ₯ 이득이 μ™œ 쒁은지)λŠ” ../ARCHITECTURE.md Β§4Β·Β§5.