반응형
.pointerInput(Unit) 사용시 주의할 점
클릭 작업 중에 onLongPress를 하기위해 pointerInput을 사용했었는데..! 문제가 발생 했다.
1. 문제 상황
Compose에서 아래처럼 pointerInput 안에서 originalItem을 직접 참조하면
리컴포즈 후에도 클릭 시 이전 데이터가 찍히는 현상이 발생할 수 있습니다.
Box(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
println("클릭 시 originalItem: $originalItem") // ⛔ 옛날 값 찍힘
}
)
}
)
2. 왜 이런 문제가 생길까? (원인)
1) pointerInput의 key 문제
- pointerInput(Unit)은 key가 Unit이므로 최초 한 번만 실행
- 이 안의 람다는 생성 시점의 originalItem을 캡처(클로저)함
- 이후 리컴포즈가 되어도 pointerInput이 재생성되지 않으면 여전히 옛날 값 참조
2) Compose의 상태 갱신 방식
- Compose는 값이 바뀌면 UI를 리컴포즈하지만,
- remember / pointerInput처럼 key가 변하지 않으면 내부 람다는 그대로 유지
- 그래서 람다 안에서 외부 변수를 직접 참조하면 stale(오래된 값) 문제가 생김
3. 해결 방법
방법 1: pointerInput key에 상태값 넣기
.pointerInput(originalItem) {
detectTapGestures {
println(originalItem) // 항상 최신 값
}
}
- key가 바뀌면 pointerInput 재생성
- 단점: originalItem이 자주 바뀌면 매번 재생성되어 성능 부담
방법 2: rememberUpdatedState 사용
val currentItem by rememberUpdatedState(originalItem)
.pointerInput(Unit) {
detectTapGestures {
println(currentItem) // 최신 값
}
}
- key는 Unit 그대로 두고
- 내부에서 항상 최신 값을 참조할 수 있도록 래핑
방법 3: 클릭 로직을 외부로 분리 (추천)
val onClick = { println(originalItem) } // 리컴포즈마다 최신 originalItem 캡처
.pointerInput(Unit) {
detectTapGestures(
onTap = { onClick() }
)
}
- pointerInput은 터치 감지만 담당
- 실제 데이터 참조는 외부 람다에서 처리 → 항상 최신 값
4. 더 안전하게: 확장함수로 추출
저는 아예 pointerInput을 확장함수로 만들어서
외부 람다를 항상 최신 상태로 호출하도록 처리했습니다.
@Composable
fun Modifier.throttleTapWithPress(
skipMs: Long = 300,
onPress: (Boolean) -> Unit = {},
onLongPress: () -> Unit = {},
onTap: (tapOffset: Offset) -> Unit,
): Modifier {
var isClickable by remember { mutableStateOf(true) }
val coroutineScope = rememberCoroutineScope()
val currentOnTap by rememberUpdatedState(onTap)
val currentOnPress by rememberUpdatedState(onPress)
val currentOnLongPress by rememberUpdatedState(onLongPress)
return pointerInput(Unit) {
detectTapGestures(
onPress = {
try {
currentOnPress(true)
tryAwaitRelease()
} finally {
currentOnPress(false)
}
},
onTap = { tapOffset ->
if (isClickable) {
isClickable = false
currentOnTap(tapOffset)
coroutineScope.launch {
delay(skipMs)
isClickable = true
}
}
},
onLongPress = {
currentOnLongPress()
}
)
}
}
사용시
Box(
modifier = Modifier
.throttleTapWithPress(
onTap = { println(originalItem) }, // 항상 최신 값
onLongPress = { println("롱프레스") }
)
)
요약
- 문제: pointerInput(Unit) 안에서 상태값을 직접 참조하면 옛날 값이 찍힘
- 원인: key가 변하지 않아 람다가 재생성되지 않고, 초기 캡처값만 유지됨
- 해결:
- key에 상태값 넣기 → pointerInput 재생성
- rememberUpdatedState로 항상 최신 값 참조
- 클릭 로직을 외부 람다로 분리(추천)
- 확장함수로 만들고 내부에서 rememberUpdatedState로 방어
반응형
'코틀린 & 컴포즈 & Java > 컴포즈 Compose' 카테고리의 다른 글
| [Compose] 스크롤 안에 (뷰 페이저 + 스크롤) 있는 경우 (0) | 2025.02.25 |
|---|---|
| [Compose] Scaffold: 내부 동작 원리와 UI 자동 조정 과정 분석 (0) | 2025.02.25 |
| [Compose] 컴포즈 실전 내가 경험한 Tip ! (0) | 2025.02.25 |
| [Compose] UI를 구성할 때 요소를 정렬하는 방법 (0) | 2025.02.10 |
| [Compose] LiveData vs StateFlow: Compose에서 어떤 것을 사용할까? (0) | 2025.02.06 |