기존 Rx에서 이중클릭/다중클릭 방지하기 위해 throttleFirst를 사용했었다.
Coroutine에서는 어떻게 이중클릭을 방지할 수 있을까?
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++)
}
CoroutineScope
과 AppCompatActivity
를 구현/상속한 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 스레드에서 처리
}
이 내용이 조금 어려운데, 찬찬히 살펴보자면..
먼저 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처럼 시간을 지정해두었다. -> 이를 코루틴을 통해 해결할 수 있음.