🤔 고민의 시작
기존의 RxJava를 사용했던 코드들을 Flow로 교체하다가 막힌 부분이 바로 throttleFirst()
이다. 다중클릭 방지 등을 위해서는 일정 시간동안 들어온 값 중에서 가장 첫번째 것만 발행하고 나머지는 무시하는 RxJava의 throttleFirst()
연산자가 필요한데, Flow에서는 해당 기능의 연산자를 지원하지 않는다. 그래서 이 연산자를 직접 구현해보기로 했다.
🆖 기존 RxJava를 사용한 다중클릭 방지 코드
private fun View.setRxBindingClicks() {
this.clicks()
.throttleFirst(1000, TimeUnit.MILLISECONDS)
.subscribe { onClick(id) }
.addTo(disposeBag)
}
private fun onClick(id: Int) {
when (id) {
R.id.______ -> clickEvent()
}
}
clicks()
: RxBinding. 클릭 이벤트를 Observable 형태로 바꿔서 원하는 Rx 연산자를 사용할 수 있도록 한다.throttleFirst(1000, TimeUnit.MILLISECONDS)
: Rx 연산자. 일정 시간 구간동안 발생한 이벤트 중 첫번째로 발생한 이벤트 발행, 나머지는 무시.
🆕 Rx → Flow
연산자 커스텀
// FlowExtensions.kt
// 클릭 이벤트를 flow로 변환
fun View.clicks(): Flow<Unit> = callbackFlow {
setOnClickListener {
this.trySend(Unit)
}
awaitClose { setOnClickListener(null) }
}
// 마지막 발행 시간과 현재 시간 비교해서 이벤트 발행, 나머지는 무시.
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { upstream ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime > windowDuration) {
lastEmissionTime = currentTime
emit(upstream)
}
}
}
⛔️ flow 연산자 중 debounce
가 있지만, 이 연산자는 특정 타임 구간 안에서 발행되는 값 중 가장 최신의 값을 발행하기 때문에 클릭이벤트 처리에는 적합하지 않다고 생각해서 throttleFirst
확장함수를 만들었다...
debounce
가 필요한 상황 예시 : 특정 ms동안 새로운 텍스트를 입력하지 않으면 search() 호출
활용
// ViewExtensions.kt
fun View.setClickEvent(
uiScope: CoroutineScope,
windowDuration: Long = THROTTLE_DURATION,
onClick: () -> Unit,
) {
clicks()
.throttleFirst(windowDuration)
.onEach { onClick.invoke() }
.launchIn(uiScope)
}
// ____Activity.kt
button.setClickEvent(lifecycleScope) {
Log.i("[TAG]", "click - ${System.currentTimeMillis()}")
}