[Android][Kotlin] 뷰 중복 클릭 방지하기 with KTX
by Yena Choi
Study Note
갑자기 그런 생각이 들 때가 있다.
이 작업 며칠 전에도 했는데, 새 화면 만들고 나니까 또 해줘야 하네?
회사에서 열심히 화면을 만들고, 뷰 클릭 처리를 달고, 테스트를 하다보니 뷰가 이중 클릭되어 예기치 못한 크래쉬가 발생했다. 그래서 액티비티로 돌아가 이중 클릭을 방지하는 코드를 넣던 중, ‘며칠 전에 했는데’ 라는 생각이 들었다. 문득 몇 주 전 컨퍼런스에서 KTX
라는 키워드와 함께 비슷한 케이스를 언급한 게 생각나서 한 번 사용해 보기로 했다.
Kotlin Extensions (KTX)
코틀린 익스텐션(Kotlin extension, KTX)은 Android Jetpack과 기타 안드로이드 라이브러리에 포함된 코틀린 확장 프로그램 세트이다. 조금 풀어서 말하자면 코틀린 코드를 더 간결하고 직관적으로 쓰도록 도와주는 기능의 모음 이라고 할 수 있겠다. 아래와 같은 여러 코틀린 기능을 활용해 코드를 좀 더 쉽게 쓸 수 있다.
- 확장 함수
- 확장 프로그램 속성
- 람다
- 이름이 지정된 매개변수
- 매개변수 기본값
- 코루틴
이 중 모든 View
에서 사용할 수 있는 확장 함수(Extension functions)를 붙여 줄 생각이다.
KTX를 사용하기 위해서 기존에는 속성을 gradle에 추가해주어야 했다. 하지만 Android Studio 3.5 버전 기준으로 새 프로젝트를 Kotlin 언어로 생성하면 자동으로 Kotlin Extension 을 사용하도록 experimental = true
build.gradle
에 추가가 되는 것 같다.
apply plugin: 'kotlin-android-extensions'
...
dependencies {
...
implementation 'androidx.core:core-ktx:1.1.0'
}
기존 처리 방식
기존에는 중복 클릭 방지가 필요했던 View마다 boolean 값을 확인하는 방식으로 처리했었다. 뷰의 OnClickListener에서 클릭을 받는 순간 isClickable
을 false로 바꾸고, 300ms 후에 다시 true로 변경하는 과정을 거쳤다.
class SomeActivity : AppCompatActivity() {
private var isClickable = true
override fun onCreate(savedInstanceState: Bundle?) {
...
view.setOnClickListener {
if (isClickable) {
isClickable = false
doSomething()
it.postDelayed({
isClickable = true
}, 300)
}
}
}
}
복잡한 코드는 아니지만, 일일이 boolean 값을 생성하고 갱신하는 것이 너무 번거로웠다. 그래서 범용적으로 쓸 수 있는 listener
를 만들고, 그 안에서 모두 처리할 수 있게 만들어보기로 했다.
해결책
- Listener 생성
- Listener 연결
- 호출
Listener 생성
View
에서 setOnClickListener
대신 쓸 클래스를 만들어 주었다. ‘중복 클릭을 방지하는 리스너’라서 변수 이름 짓기 너무 힘들었는데 Throttle 이라는 단어를 사용하기로 했다. (예제를 찾아보니 Debounce, SingleClick 등등의 용어도 사용하는 것 같다.)
자꾸 생각하다보니 너무 궁금해져서 Throttle
과 Debounce
의 작동 원리와 차이점을 찾아보았다. Throttle은 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것, Debounce는 이벤트를 그룹화하여 특정시간이 지난 후 하나의 이벤트만 발생하도록 하는 기술 이라고 한다. 내가 쓰는 방식은 한 번 호출 후 일정 시간 동안 호출을 막는 것이므로 Throttle 에 가까워 이 네이밍 그대로 가기로 했다.
참고 : https://webclub.tistory.com/607
기본 클릭 간격(인터벌)은 이전과 마찬가지로 300ms로 주고, 필요할 때에는 초기화 시에 다른 값을 넣어 사용할 수 있도록 했다.
// OnThrottleClickListener.kt
class OnThrottleClickListener(
private val clickListener: View.OnClickListener,
private val interval: Long = 300
) :
View.OnClickListener {
private var clickable = true
// clickable 플래그를 이 클래스가 아니라 더 상위 클래스에 두면
// 여러 뷰에 대한 중복 클릭 방지할 수 있다.
override fun onClick(v: View?) {
if (clickable) {
clickable = false
v?.run {
postDelayed({
clickable = true
}, interval)
clickListener.onClick(v)
}
} else {
Log.d(TAG, "waiting for a while")
}
}
}
View.OnClickListener
를 상속 받아 클릭 이벤트를 감지한다. 만약 클릭이 발생할 경우 onClick
에서 클릭 가능 여부를 판단하고, 인자로 받은 clickListener를 통해 이벤트를 실행한다.
clickable이 true일 때에만 클릭을 실행하는데, 이 플래그가 클릭리스너 내부에 있기 때문에 버튼A와 버튼B를 연속 클릭 했을 때에는 각각의 플래그가 적용되어 A와 B가 둘 다 클릭될 것이다. 때문에 여러 뷰의 중복 클릭을 방지하고 싶다면 플래그를 더 상위 클래스로 옮겨야 한다.
Listener 연결
다음으로, MainActivity 에서 onCreate 시 위에서 만든 ThrottleClickListener를 세팅해주는 함수가 필요하다. KTX를 통해 View에서 쓸 수 있는 함수를 만들어 리스너를 세팅해주었다. (만드는 방법은 특별한 건 없고… 그냥 만드니 동작했다고 한다) 너무 간단해서 KTX 사용 안 한 줄 알고 gradle을 수십 번 확인했다…
(View) -> Unit 람다 식을 파라미터로 받고, 새로운 View.OnClickListener
를 만들어서 onClick 에서의 액션으로 지정해주었다. 또한 클릭 간격 조정이 필요할 때를 위해 interval도 세팅할 수 있게 오버로드 해주었다.
// OnThrottleClickListener.kt
fun View.onThrottleClick(action: (v: View) -> Unit) {
val listener = View.OnClickListener { action(it) }
setOnClickListener(OnThrottleClickListener(listener))
}
// with interval setting
fun View.onThrottleClick(action: (v: View) -> Unit, interval: Long) {
val listener = View.OnClickListener { action(it) }
setOnClickListener(OnThrottleClickListener(listener, interval))
}
호출
이렇게까지 만들어 두었으면 뷰에서 사용하기는 간단하다. 바로 호출.
// MainActivity.kt
button.onThrottleClick {
Log.d(TAG, "button Clicked : ${++btnCount}")
}
로그 창을 보면 버튼을 광클했음에도 0.3초가 지나기 전에는 숫자 카운트가 되지 않는다.
결론
중복 클릭 막는 문제는 보통 Rx나 Coroutine으로도 많이 처리하는 것 같다. 나는 아직 코루틴은 써보지 않았고, Rx로는 써봤지만 ‘굳이 클릭 처리 하려고 구독하고 해제까지?’라는 생각이 들었다. 보고 쓰고 관리하기에 깔끔하다면 클래식하더라도 단순한 코드가 좋은 것 같다. 그리고 단순한 코드를 만들 수 있게 해준 KTX에 대해 더 공부해봐야 겠다는 생각이 들었다. 음.
닭 잡는 칼은 닭에, 소 잡는 칼은 소에.
References
- https://medium.com/@masaaki.iwaguchi/android-how-to-prevent-multiple-view-clicks-using-databinding-and-kotlin-extensions-5d5859071b4b
- https://medium.com/@fornewid/android-ktx-databinding%EC%9C%BC%EB%A1%9C-view-layer-%EC%BD%94%EB%93%9C-%EC%A4%84%EC%9D%B4%EA%B8%B0-%EF%B8%8F-690cd61aec3d