Skip to content

Conversation

@easyhooon
Copy link
Contributor

@easyhooon easyhooon commented Feb 3, 2026

🔗 관련 이슈

📙 작업 설명

  • composed {} -> Modifier.Node API
    • noRippleClickable: composed {} 제거 → clickable(interactionSource = null, indication = null) 직접 호출
  • clickableSingle: composed {} 제거 → ModifierNodeElement + DelegatingNode 패턴
  • MultipleEventsCutter - Node 인스턴스 필드로 유지 (remember 불필요)
    • 리플 - ripple().create(interactionSource) delegate
    • 탭 감지 - SuspendingPointerInputModifierNode delegate
    • 접근성 - SemanticsModifierNode 구현
    • disabled - 포인터 입력 블록 자체를 early return으로 차단

🧪 테스트 내역 (선택)

  • 주요 기능 정상 동작 확인
  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

💬 추가 설명 or 리뷰 포인트 (선택)

composed {}는 내부적으로 materialize() 과정을 거쳐 별도의 Composition을 생성합니다. 이를 통해 remember, CompositionLocal 등 Composable API를 사용할 수 있지만,

  1. materialize 오버헤드 — Modifier 적용 시마다 materialize()가 호출되어 내부적으로 Composition을 생성하고 상태를 초기화하는 비용이 발생
  2. 컴파일러 최적화 불가 — composed 내부 람다는 skippable하지 않아 캐싱이 안됨
  3. Google이 공식 문서에서 비권장(no longer recommended)으로 분류

Modifier.Node가 해결하는 방식

composed {} Modifier.Node
상태 유지 remember (Composition 필요) Node 인스턴스 필드 (Composition 불필요)
Composition 생성 적용 시마다 materialize 없음
노드 재사용 매번 새로 생성 create() 1 회, 이후 update()로 갱신
컴파일러 최적화 불가 ModifierNodeElement의 equals로 skip 가능

Modifier.Node는 Node 인스턴스가 직접 상태를 보유하므로 remember나 Composition 없이 값을 유지하고, update()를 통해 변경 사항만 반영합니다.

reference)
https://developer.android.com/develop/ui/compose/custom-modifiers?utm_source=android-studio-app&utm_medium=app#implement-custom
coil-kt/coil#2115
https://blog.naver.com/tgyuu_/224143006432
https://kimansu.medium.com/compose-modifier-node-4390861c9e68

Summary by CodeRabbit

  • 새로운 기능

    • 리플 효과 없는 클릭 수정자 옵션(noRippleClickable) 추가
  • 개선사항

    • 단일 클릭 처리 흐름 개선으로 더 안정적인 클릭 동작 보장
    • 포인터 입력 감지 및 응답성 향상
    • 중복/여러 이벤트 차단으로 불필요한 중복 호출 방지
    • 접근성(레이블·활성/비활성 상태) 처리 강화

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Walkthrough

composed 기반의 clickableSingle 구현을 노드 기반(ClickableSingleElement/ClickableSingleNode)으로 마이그레이션하고, 포인터 입력 처리, 리플 인디케이션, 의미론(Semantics) 및 이벤트 컷팅을 내부 노드로 재배치했으며 noRippleClickable을 추가했습니다.

Changes

Cohort / File(s) Summary
Modifier 확장 리팩토링
core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt
clickableSingle을 composed에서 노드 기반(ClickableSingleElement, ClickableSingleNode)으로 변경. 내부에 SuspendingPointerInputModifierNode(포인터/제스처 처리), 리플 인디케이터 노드, SemanticsModifierNode(onClick/label/enabled 처리), MultipleEventsCutter(중복 이벤트 차단) 등을 도입. noRippleClickable(onClick: () -> Unit) 퍼블릭 API 추가.

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI Layer
  participant ModifierNode as ClickableSingleNode
  participant Pointer as SuspendingPointerInputModifierNode
  participant Ripple as RippleNode
  participant Semantics as SemanticsModifierNode
  participant Cutter as MultipleEventsCutter

  UI->>ModifierNode: 클릭 가능 요소 렌더링
  ModifierNode->>Semantics: 의미론 등록(onClick, label, enabled)
  ModifierNode->>Pointer: 포인터 이벤트 수신 등록
  Pointer->>ModifierNode: pointerDown / pointerUp 이벤트
  Pointer->>Cutter: 전달(중복 차단)
  Cutter->>ModifierNode: onTap 트리거
  ModifierNode->>Ripple: 리플 표시 시작
  ModifierNode->>Semantics: PressInteraction/State 업데이트
  ModifierNode->>UI: onClick 콜백 실행
  Ripple->>ModifierNode: 리플 종료
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

토끼가 새 노드를 심었네,
클릭은 부드럽게, 리플은 살짝,
포인터와 의미가 손을 잡고,
이벤트는 가지런히 줄서고,
작지만 반짝이는 변경의 숲 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed 모든 코드 수준의 요구사항이 충족되었습니다. composed {} 제거, Modifier.Node 기반 구현, MultipleEventsCutter 상태 보유, 및 ripple/pointer input/semantics 위임이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #262 이슈의 범위 내입니다. composed {} 마이그레이션과 Modifier.Node 기반 구현이 목표이며, 변경사항이 이를 정확히 따릅니다.
Title check ✅ Passed 제목은 composed {} 기반 구현을 Modifier.Node API 사용 방식으로 변경하는 PR의 주요 목표를 명확하게 요약하고 있으며, 변경사항 요약과 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-501-refactor/#262

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt`:
- Around line 71-88: 현재 Modifier에서 shouldAutoInvalidate = false로 설정해 놓고
update()에서 invalidate()를 호출하지 않아 enabled/label/role 변경 시 포인터 입력 재평가와
applySemantics() 갱신이 누락됩니다; 해결 방법은 (1) shouldAutoInvalidate를 true로 되돌리거나 (2)
update() 내부에서 이전 상태와 현재 상태(enabled, label, role 등)를 비교해 변경이 감지되면 invalidate()를
호출하도록 변경하세요; 관련 식별자: SuspendingPointerInputModifierNode, update(), invalidate(),
shouldAutoInvalidate, applySemantics()을 찾아 적용하고 동일한 수정은 104-110 범위에도 반영하세요.
- Around line 91-97: The semantics onClick lambda always returns true regardless
of enabled state; update SemanticsPropertyReceiver.applySemantics so the
onClick(label = onClickLabel) handler only calls
multipleEventsCutter.processEvent { this@ClickableSingleNode.onClick() } and
returns true when enabled is true, and returns false when enabled is false
(i.e., make the returned boolean reflect enabled), so the accessibility service
receives the correct success/failure result.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt`:
- Around line 69-89: The pointer input coroutine started by pointerInputNode's
SuspendingPointerInputModifierNode doesn't restart when enabled toggles; modify
the update() implementation to detect changes to the enabled property and call
resetPointerInputHandler() when enabled value differs from previous state so the
detectTapGestures coroutine is restarted; locate pointerInputNode, the
SuspendingPointerInputModifierNode block, and the update() method and ensure you
compare the old and new enabled values and invoke resetPointerInputHandler() on
change.

Comment on lines +69 to +89
@Suppress("unused")
private val pointerInputNode = delegate(
SuspendingPointerInputModifierNode {
if (!enabled) return@SuspendingPointerInputModifierNode
detectTapGestures(
onPress = { offset ->
val press = PressInteraction.Press(offset)
interactionSource.emit(press)
val released = tryAwaitRelease()
if (released) {
interactionSource.emit(PressInteraction.Release(press))
} else {
interactionSource.emit(PressInteraction.Cancel(press))
}
},
onTap = {
multipleEventsCutter.processEvent { onClick() }
},
)
},
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

enabled 상태 변경 시 포인터 입력 핸들러가 재시작되지 않습니다.

SuspendingPointerInputModifierNode의 코루틴은 한 번 시작되면 enabled 값이 변경되어도 자동으로 재시작되지 않습니다. enabledtrue에서 false로 변경되어도 이미 실행 중인 detectTapGestures 코루틴이 계속 탭을 처리합니다.

update() 메서드에서 enabled 상태 변경 시 resetPointerInputHandler()를 호출해야 합니다.

🩹 수정 제안
-    `@Suppress`("unused")
     private val pointerInputNode = delegate(
         SuspendingPointerInputModifierNode {
             if (!enabled) return@SuspendingPointerInputModifierNode
             detectTapGestures(
                 onPress = { offset ->
                     val press = PressInteraction.Press(offset)
                     interactionSource.emit(press)
                     val released = tryAwaitRelease()
                     if (released) {
                         interactionSource.emit(PressInteraction.Release(press))
                     } else {
                         interactionSource.emit(PressInteraction.Cancel(press))
                     }
                 },
                 onTap = {
                     multipleEventsCutter.processEvent { onClick() }
                 },
             )
         },
     )

     fun update(enabled: Boolean, onClickLabel: String?, role: Role?, onClick: () -> Unit) {
+        val enabledChanged = this.enabled != enabled
         this.enabled = enabled
         this.onClickLabel = onClickLabel
         this.role = role
         this.onClick = onClick
+        if (enabledChanged) {
+            pointerInputNode.resetPointerInputHandler()
+        }
     }

Also applies to: 103-108

🤖 Prompt for AI Agents
In
`@core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt`
around lines 69 - 89, The pointer input coroutine started by pointerInputNode's
SuspendingPointerInputModifierNode doesn't restart when enabled toggles; modify
the update() implementation to detect changes to the enabled property and call
resetPointerInputHandler() when enabled value differs from previous state so the
detectTapGestures coroutine is restarted; locate pointerInputNode, the
SuspendingPointerInputModifierNode block, and the update() method and ensure you
compare the old and new enabled values and invoke resetPointerInputHandler() on
change.

@easyhooon easyhooon changed the title [BOOK-501] refactor: composed {} 를 이용한 Custom Modifier 구현 Modifier.Node API 사용 방식으로 변경 refactor: composed {} 를 이용한 Custom Modifier 구현 Modifier.Node API 사용 방식으로 변경 Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-501/refactor] composed {} 사용 코드 마이그레이션

2 participants