Skip to content

Commit

Permalink
Merge pull request #317 from Hous-Release/feature/#277-compose-single…
Browse files Browse the repository at this point in the history
…-click

#277 [Feat] 컴포즈 SingleEventHandler 구현 및 적용
  • Loading branch information
murjune authored Sep 19, 2023
2 parents 59da635 + 33a6630 commit 07f3766
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package hous.release.android.presentation.our_rules.component.update

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
Expand All @@ -21,6 +20,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import hous.release.designsystem.R
import hous.release.designsystem.modifier.clickableSingle
import hous.release.designsystem.theme.HousBlue
import hous.release.designsystem.theme.HousG4
import hous.release.designsystem.theme.HousTheme
Expand All @@ -33,7 +33,7 @@ fun RuleAddPhotoButton(
) {
val focusManager = LocalFocusManager.current
Row(
modifier = Modifier.clickable(
modifier = Modifier.clickableSingle(
enabled = isActiveButton,
onClick = {
focusManager.clearFocus()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ fun RulePhotoStatusBar(
onOpenGallery: () -> Unit = {}
) {
Row(
modifier = modifier.fillMaxWidth().padding(start = 16.dp, end = 28.dp),
modifier = modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 28.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
RuleAddPhotoButton(photoCount in (0..4), onOpenGallery)
RuleAddPhotoButton(
isActiveButton = photoCount in (0..4),
onClick = onOpenGallery
)

Text(
text = "$photoCount/5",
Expand Down
13 changes: 9 additions & 4 deletions designsystem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ android {
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
java {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}

kotlinOptions {
jvmTarget = "1.8"
jvmTarget = jvmVersion
}

buildFeatures {
Expand All @@ -42,6 +43,8 @@ android {
}

dependencies {
implementation(project(":testing"))

Deps.AndroidX.Compose.run {
implementation(activity)
implementation(material)
Expand All @@ -51,6 +54,8 @@ dependencies {
implementation(mdcTheme)
implementation(appCompatTheme)
}
testImplementation()
androidTestImplementation()
}

ktlint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import hous.release.designsystem.R
import hous.release.designsystem.component.user_interaction.SingleEventArea
import hous.release.designsystem.theme.HousBlue
import hous.release.designsystem.theme.HousWhite

Expand All @@ -38,28 +39,31 @@ fun FabScreenSlot(
fun HousFloatingButton(
onClick: () -> Unit = {}
) {
FloatingActionButton(
onClick = onClick,
backgroundColor = HousBlue,
contentColor = HousWhite,
modifier = Modifier.size(92.dp).padding(16.dp)
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus),
contentDescription = "Add"
)
SingleEventArea { cutter ->
FloatingActionButton(
onClick = { cutter.handle(onClick) },
backgroundColor = HousBlue,
contentColor = HousWhite,
modifier = Modifier.size(92.dp)
.padding(16.dp)
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus),
contentDescription = "Add"
)
}
}
}

@Composable
@Preview(showBackground = true)
fun Preview() {
private fun Preview() {
HousFloatingButton()
}

@Composable
@Preview(widthDp = 360, heightDp = 640, showBackground = true)
fun PreviewFabContainerWithContent() {
private fun PreviewFabContainerWithContent() {
FabScreenSlot(
fabOnClick = {},
content = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hous.release.designsystem.component.user_interaction

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import hous.release.designsystem.util.single_event.SingleEventHandler

/**
* 여러 번 클릭 이벤트를 막아주는 Wrapper Composable
* */
@Composable
fun <T> SingleEventArea(
content: @Composable (SingleEventHandler) -> T
) {
val singleEventHandler = remember { SingleEventHandler() }

content(singleEventHandler)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package hous.release.designsystem.modifier

import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
import hous.release.designsystem.util.single_event.SingleEventHandler

/**
* 여러 번 클릭 이벤트를 막아주는 Modifier
* */
fun Modifier.clickableSingle(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
val manager = remember { SingleEventHandler() }
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = { manager.handle { onClick() } },
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package hous.release.designsystem.util
package hous.release.designsystem.util.extension

import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package hous.release.designsystem.util.single_event

fun interface SingleEventHandleStrategy {
fun handle(event: () -> Unit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package hous.release.designsystem.util.single_event

class SingleEventHandler(
private val singleEventHandleStrategy: SingleEventHandleStrategy = ThrottledDurationStrategy()
) {
fun handle(event: () -> Unit) {
singleEventHandleStrategy.handle(event)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package hous.release.designsystem.util.single_event

import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
import kotlin.time.TimeMark
import kotlin.time.TimeSource

@OptIn(ExperimentalTime::class)
internal class ThrottledDurationStrategy : SingleEventHandleStrategy {
private val currentTime: TimeMark get() = TimeSource.Monotonic.markNow()
private val throttleDuration: Duration = 300.milliseconds
private lateinit var lastEventTime: TimeMark

override fun handle(event: () -> Unit) {
if (::lastEventTime.isInitialized.not() || (lastEventTime + throttleDuration).hasPassedNow()) {
event()
}
lastEventTime = currentTime
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package hous.release.designsystem.util.single_event

import hous.release.testing.CoroutinesTestExtension
import io.mockk.every
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(CoroutinesTestExtension::class)
@ExtendWith(MockKExtension::class)
internal class SingleEventHandlerTest {

@Test
fun `이벤트를 처리할 때 내부적으로 SingleEventHandleStrategy의 handle을 호출 한다`() {
// given
val mockStrategy = mockk<SingleEventHandleStrategy>(relaxed = true)
val handler = SingleEventHandler(mockStrategy)

// when
every { mockStrategy.handle(any()) } returns Unit
handler.handle { }
// then
verify(exactly = 1) { handler.handle(any()) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package hous.release.designsystem.util.single_event

import com.google.common.truth.Truth
import hous.release.testing.CoroutinesTestExtension
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(CoroutinesTestExtension::class)
internal class ThrottledDurationStrategyTest {

@Test
fun `마지막 이벤트가 발생한 지 300ms가 지나지 않고 추가 이벤트가 발생시 무시한다`() = runTest {
// given
val strategy = ThrottledDurationStrategy()
var count = 0
// when
strategy.handle {
count++
}
delay(301)
strategy.handle {
count++
}
// then
Truth.assertThat(count).isEqualTo(1)
}
}

0 comments on commit 07f3766

Please sign in to comment.