Android/Coroutine

Rx 말고, Flow로 다중클릭 방지하기 (throttleFirst)

MJ핫산 2022. 9. 25. 14:42

🤔 고민의 시작

기존의 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()}")
}

🔗 참고