Jetpack Compose에서 다중 클릭 방지

매일 수정하는 GNOSS LV5·2022년 3월 21일
2

AndroidStudio

목록 보기
63/83
post-custom-banner

https://medium.com/@al-e-shevelev/how-to-prevent-multiple-clicks-in-android-jetpack-compose-8e62224c9c5e

첫번째 방법

코루틴에 기반을 둔 방법이 있다.

interface MultipleEventsCutterManager {
    fun processEvent(event: () -> Unit)
}

@OptIn(FlowPreview::class)
@Composable
fun <T>multipleEventsCutter(
    content: @Composable (MultipleEventsCutterManager) -> T
) : T {
    val debounceState = remember {
        MutableSharedFlow<() -> Unit>(
            replay = 0,
            extraBufferCapacity = 1,
            onBufferOverflow = BufferOverflow.DROP_OLDEST
        )
    }

    val result = content(
        object : MultipleEventsCutterManager {
            override fun processEvent(event: () -> Unit) {
                debounceState.tryEmit(event)
            }
        }
    )

    LaunchedEffect(true) {
        debounceState
            .debounce(300L)
            .collect { onClick ->
                onClick.invoke()
            }
    }

    return result
}

이 방식에서는 SharedFlow를 생성한다.
SharedFlow는 생성시 설정을 할 수 있다.
replay를 이용하여 이전에 보낸 값들을 다시 보낼지를 정할 수 있고 onBufferOverflow를 통하여 버퍼가 가득 찰 경우 어떻게 할지 정할 수 있다. DROP_OLDEST를 통하여 스택에 쌓인 오래된 순서대로 버려낼 수 있다.
그리고 debounce를 이용하여 가장 최근에 클릭한 이벤트를 받을 수 있다.

Modifier를 이용하면 다음과 같이 사용한다.

fun Modifier.clickableSingle(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onClick: () -> Unit
) = composed(
    inspectorInfo = debugInspectorInfo {
        name = "clickable"
        properties["enabled"] = enabled
        properties["onClickLabel"] = onClickLabel
        properties["role"] = role
        properties["onClick"] = onClick
    }
) {
    multipleEventsCutter { manager ->
        Modifier.clickable(
            enabled = enabled,
            onClickLabel = onClickLabel,
            onClick = { manager.processEvent { onClick() } },
            role = role,
            indication = LocalIndication.current,
            interactionSource = remember { MutableInteractionSource() }
        )
    }
}

두번째 방법

두번째 방법은 첫 클릭과 다음 클릭의 텀을 계산하는 방식이다.

internal interface MultipleEventsCutter {
    fun processEvent(event: () -> Unit)

    companion object
}

internal fun MultipleEventsCutter.Companion.get(): MultipleEventsCutter = 
    MultipleEventsCutterImpl()

private class MultipleEventsCutterImpl : MultipleEventsCutter {
    private val now: Long
        get() = System.currentTimeMillis()

    private var lastEventTimeMs: Long = 0

    override fun processEvent(event: () -> Unit) {
        if (now - lastEventTimeMs >= 300L) {
            event.invoke()
        }
        lastEventTimeMs = now
    }
}

코루틴 대신 시간 비교에 기반을 두었다. now 와 lastEventTimeMs를 비교하여 역치를 넘을 경우 이벤트를 실행시키는 방식이다.

이것 또한 Modifier에 적용이 가능하다.

fun Modifier.clickableSingle(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onClick: () -> Unit
) = composed(
    inspectorInfo = debugInspectorInfo {
        name = "clickable"
        properties["enabled"] = enabled
        properties["onClickLabel"] = onClickLabel
        properties["role"] = role
        properties["onClick"] = onClick
    }
) {
    val multipleEventsCutter = remember { MultipleEventsCutter.get() }
    Modifier.clickable(
        enabled = enabled,
        onClickLabel = onClickLabel,
        onClick = { multipleEventsCutter.processEvent { onClick() } },
        role = role,
        indication = LocalIndication.current,
        interactionSource = remember { MutableInteractionSource() }
    )
}
profile
러닝커브를 따라서 등반중입니다.
post-custom-banner

0개의 댓글