throttleFirst 대체 Coroutine에서 다중클릭/이중클릭 방지

기존 Rx에서 이중클릭/다중클릭 방지하기 위해 throttleFirst를 사용했었다.

Coroutine에서는 어떻게 이중클릭을 방지할 수 있을까?

Actor를 사용하자

핵심 개념

private fun View.onClickOnce(action: suspend (View) -> Unit) {
	val event = GlobalScope.actor<View>(Dispathers.Main) {
    	for (event in channel) 
        	action(event)
    }
    
    setOnClickListner {
    	event.offer(it)
    }
}

var currentIndex = 0
fab.onClickOnce {
	10.countDown(currentIndex++)
}

실제 사용방법

1. BaseOneClickActivity 만들기

CoroutineScopeAppCompatActivity를 구현/상속한 BaseOneClickActivity를 만든다. 그리고 앞으로 이중클릭 방지가 필요한 액티비티는 BaseOneClickActivity 를 상속받아 이용한다.

abstract class BaseOneClickActivity: AppCompatActivity(). CoroutineScope {
	private val job: Job = Job()
    
    override val coroutineContext: CoroutineContext
    	get() = Dispatchers.Main + job
        
    override fun onDestroy() {
    	super.onDestroy()
        job.cancel()
    }
}

abstract class MainActivity : BaseOneClickActivity() {
	launch{} // UI 스레드에서 처리
    launch(Dispatchers.Default) {} // Default 스레드에서 처리
    
    actor<Generic Type> {} // UI 스레드에서 처리
    actor<Generic Type>(Dispatchers.Default) {} // Default 스레드에서 처리
}

2. onClick만들기.

이 내용이 조금 어려운데, 찬찬히 살펴보자면..

먼저 fun <E> View.onClick을 보면, 여기서 CoroutinesSendChannelOnClickEvent이 클래스를 만들어 반환해주고 있다. 이 클래스 안에 consumeEach() 함수가 있는데, 코드 최하단에 보면 infix 함수로 .consume()함수가 있다. 이 함수 내부에서 consumeEach()를 사용하고 있다.

결국 이는 아래 사용법 코드에서처럼 코드를 예쁘게 쓰기 위해 추가된 함수다!

뷰.onClick(디스패쳐){
해당 디스패쳐에서 수행할 내용
} consume { // infix 함수로 되어있어서 이런 패턴으로 이용가능.
메인 스레드에서 수행할 내용
}

class CoroutinesSendChannelOnClickEvent<E>(
    private val view: View,							// View: click을 위한 View
    private val bgBody: suspend (item: View) -> E,	// background에서 실행할 내용
    private val dispatcherProvider: DispatchersProviderSealed = DispatchersProvider,	
    private val job: Job? = null) {					// Job : cancel을 위한 job 추가
    
    fun consumeEach(uiBody: (item: E) -> Unit): CoroutinesSendChannelOnClickEvent<E> {
        val clickActor = CoroutineScope(dispatcherProvider.main + (job ?: EmptyCoroutineContext)).actor<View> {
            this.channel.map(context = dispatcherProvider.default, transform = bgBody).consumeEach(uiBody)
        }
        view.setOnClickListener { clickActor.offer(it) }	// Offer 처리를 위한 CoroutineScope 생성
        return this
    }
}
fun <E> View.onClick(dispatcherProvider: DispatchersProviderSealed = DispatchersProvider,
                     job: Job? = null, bgBody: suspend (item: View) -> E): CoroutinesSendChannelOnClickEvent<E> =
CoroutinesSendChannelOnClickEvent(this, bgBody, dispatcherProvider, job)

infix fun <E> CoroutinesSendChannelOnClickEvent<E>.consume(uiBody: (item: E) -> Unit) {	// 생성을 간단하게 하기 위한 function 2개
    this.consumeEach(uiBody)
}

사용법은 아래와 같다.

fab.onClick(job = job) {		// click을 처리하고, background에서 loadnetwork()
    loadNetwork()
} consume {
    tv_message.text = it
}

private suspend fun loadNetwork(): String {		// Temp load network
    delay(300)
    return "currentIndex ${currentIndex++}"
}

장점

기존 Rx의 throttleFirst를 사용할 때는 각 뷰의 클릭이벤트의 시간을 정확하게 판단할 수 없으므로 임의로 500ms처럼 시간을 지정해두었다. -> 이를 코루틴을 통해 해결할 수 있음.

좋은 웹페이지 즐겨찾기