Skip to content

Commit

Permalink
공유하기 API 중복 호출 방지 로직 추가 (#82)
Browse files Browse the repository at this point in the history
* UiText core/ui 모듈로 이동

feature/complete 에서도 사용하기 위함

* 목표 달성 화면 공유하기 API Job과 ShareUrl를  통한 중복 호출 방지 로직 추가

* 목표 달성 화면 사용하지 않는 onShowSnackbar 함수 제거

* 반다라트 버튼에 Throttle 적용

0.5초 동안 연속적으로 버튼의 입력이 들어올 경우 이를 처음을 제외하고 전부 취소 시킴
바텀시트가 올라올 동안 중복으로 버튼이 호출될 수 있는 상황을 방지

* chore: 사용하지 않는 의존성 제거

* 홈 화면 공유하기 버튼에도 throttle 적용

* style check success
  • Loading branch information
easyhooon committed Aug 22, 2023
1 parent bcd1eae commit 34f2a47
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ fun BandalartNavHost(
)
completeScreen(
onNavigateBack = navController::popBackStack,
onShowSnackbar = onShowSnackbar,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@file:OptIn(InternalAPI::class)

package com.nexters.bandalart.android.core.data.remote.datasource

import com.nexters.bandalart.android.core.data.datasource.BandalartRemoteDataSource
Expand All @@ -19,7 +17,6 @@ import io.ktor.client.request.get
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.util.InternalAPI
import javax.inject.Inject

internal class BandalartRemoteDataSourceImpl @Inject constructor(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.nexters.bandalart.android.core.ui.component

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nexters.bandalart.android.core.ui.extension.clickableSingle
import com.nexters.bandalart.android.core.ui.extension.nonScaleSp
import com.nexters.bandalart.android.core.ui.theme.Gray900
import com.nexters.bandalart.android.core.ui.theme.White
Expand All @@ -22,16 +26,16 @@ fun BandalartButton(
text: String,
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
Box(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Gray900,
contentColor = Gray900,
),
.height(56.dp)
.clip(shape = RoundedCornerShape(50.dp))
.clickableSingle(onClick = onClick)
.background(color = Gray900)
.padding(16.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = text,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.nexters.bandalart.android.core.ui.extension

import android.annotation.SuppressLint
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

@SuppressLint("ModifierFactoryUnreferencedReceiver")
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
Expand All @@ -16,3 +19,28 @@ inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier
onClick()
}
}

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() },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.nexters.bandalart.android.core.ui.extension

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 >= 500L) {
event.invoke()
}
lastEventTimeMs = now
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.nexters.bandalart.android.feature.home.util
package com.nexters.bandalart.android.core.ui.extension

import android.content.Context
import androidx.annotation.StringRes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nexters.bandalart.android.feature.complete

import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
Expand Down Expand Up @@ -55,16 +56,16 @@ import com.nexters.bandalart.android.core.ui.theme.Gray900
internal fun CompleteRoute(
modifier: Modifier = Modifier,
onNavigateBack: () -> Unit,
onShowSnackbar: suspend (String) -> Boolean,
viewModel: CompleteViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

LaunchedEffect(viewModel) {
viewModel.eventFlow.collect { event ->
when (event) {
is CompleteUiEvent.ShowSnackbar -> {
onShowSnackbar(event.message)
is CompleteUiEvent.ShowToast -> {
Toast.makeText(context, event.message.asString(context), Toast.LENGTH_SHORT).show()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nexters.bandalart.android.core.domain.usecase.bandalart.ShareBandalartUseCase
import com.nexters.bandalart.android.core.domain.usecase.bandalart.UpsertBandalartKeyUseCase
import com.nexters.bandalart.android.core.ui.extension.UiText
import com.nexters.bandalart.android.feature.complete.navigation.BANDALART_KEY
import com.nexters.bandalart.android.feature.complete.navigation.BANDALART_PROFILE_EMOJI
import com.nexters.bandalart.android.feature.complete.navigation.BANDALART_TITLE
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
Expand Down Expand Up @@ -38,7 +40,7 @@ data class CompleteUiState(
)

sealed class CompleteUiEvent {
data class ShowSnackbar(val message: String) : CompleteUiEvent()
data class ShowToast(val message: UiText) : CompleteUiEvent()
}

@HiltViewModel
Expand All @@ -47,6 +49,7 @@ class CompleteViewModel @Inject constructor(
private val upsertBandalartKeyUseCase: UpsertBandalartKeyUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private var shareBandalartJob: Job? = null

private val key = savedStateHandle[BANDALART_KEY] ?: ""
private val title = savedStateHandle[BANDALART_TITLE] ?: ""
Expand Down Expand Up @@ -74,7 +77,10 @@ class CompleteViewModel @Inject constructor(
}

fun shareBandalart() {
viewModelScope.launch {
if (shareBandalartJob?.isActive == true && _uiState.value.shareUrl.isNotEmpty()) {
return
}
shareBandalartJob = viewModelScope.launch {
val result = shareBandalartUseCase(key)
when {
result.isSuccess && result.getOrNull() != null -> {
Expand All @@ -91,9 +97,11 @@ class CompleteViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(
error = exception,
)
_eventFlow.emit(CompleteUiEvent.ShowToast(UiText.DirectString(exception.message.toString())))
Timber.e(exception)
}
}
shareBandalartJob = null
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ fun NavController.navigateToComplete(

fun NavGraphBuilder.completeScreen(
onNavigateBack: () -> Unit,
onShowSnackbar: suspend (String) -> Boolean,
) {
composable(
route = COMPLETE_NAVIGATION_ROUTE,
Expand All @@ -42,7 +41,6 @@ fun NavGraphBuilder.completeScreen(
) {
CompleteRoute(
onNavigateBack = onNavigateBack,
onShowSnackbar = onShowSnackbar,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import com.nexters.bandalart.android.feature.home.model.BandalartCellUiModel
import com.nexters.bandalart.android.feature.home.model.UpdateBandalartMainCellModel
import com.nexters.bandalart.android.feature.home.model.UpdateBandalartSubCellModel
import com.nexters.bandalart.android.feature.home.model.UpdateBandalartTaskCellModel
import com.nexters.bandalart.android.feature.home.util.UiText
import com.nexters.bandalart.android.core.ui.extension.UiText
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import com.nexters.bandalart.android.core.ui.component.FixedSizeText
import com.nexters.bandalart.android.core.ui.component.LoadingScreen
import com.nexters.bandalart.android.core.ui.component.NetworkErrorAlertDialog
import com.nexters.bandalart.android.core.ui.extension.ThemeColor
import com.nexters.bandalart.android.core.ui.extension.clickableSingle
import com.nexters.bandalart.android.core.ui.extension.nonScaleSp
import com.nexters.bandalart.android.core.ui.extension.toColor
import com.nexters.bandalart.android.core.ui.extension.toFormatDate
Expand Down Expand Up @@ -471,7 +472,7 @@ internal fun HomeScreen(
.wrapContentSize()
.clip(RoundedCornerShape(18.dp))
.background(Gray100)
.clickable { uiState.bandalartDetailData?.let { shareBandalart(it.key) } }
.clickableSingle { uiState.bandalartDetailData?.let { shareBandalart(it.key) } }
.align(Alignment.CenterHorizontally),
contentAlignment = Alignment.Center,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.nexters.bandalart.android.feature.home.mapper.toUiModel
import com.nexters.bandalart.android.feature.home.model.BandalartCellUiModel
import com.nexters.bandalart.android.feature.home.model.BandalartDetailUiModel
import com.nexters.bandalart.android.feature.home.model.UpdateBandalartEmojiModel
import com.nexters.bandalart.android.feature.home.util.UiText
import com.nexters.bandalart.android.core.ui.extension.UiText
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.async
Expand Down Expand Up @@ -367,31 +367,32 @@ class HomeViewModel @Inject constructor(
}

fun shareBandalart(bandalartKey: String) {
if (!_uiState.value.isNetworking) {
_uiState.value = _uiState.value.copy(isNetworking = true)
viewModelScope.launch {
val result = shareBandalartUseCase(bandalartKey)
when {
result.isSuccess && result.getOrNull() != null -> {
_uiState.value = _uiState.value.copy(
shareUrl = result.getOrNull()!!.shareUrl,
error = null,
)
}
result.isSuccess && result.getOrNull() == null -> {
Timber.e("Request succeeded but data validation failed")
}
result.isFailure -> {
val exception = result.exceptionOrNull()!!
_uiState.value = _uiState.value.copy(
error = exception,
isNetworking = false,
)
_eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString())))
Timber.e(exception.message)
}
if (uiState.value.isNetworking || _uiState.value.shareUrl.isNotEmpty())
return

_uiState.value = _uiState.value.copy(isNetworking = true)
viewModelScope.launch {
val result = shareBandalartUseCase(bandalartKey)
when {
result.isSuccess && result.getOrNull() != null -> {
_uiState.value = _uiState.value.copy(
shareUrl = result.getOrNull()!!.shareUrl,
error = null,
)
}
result.isSuccess && result.getOrNull() == null -> {
Timber.e("Request succeeded but data validation failed")
}
result.isFailure -> {
val exception = result.exceptionOrNull()!!
_uiState.value = _uiState.value.copy(
error = exception,
)
_eventFlow.emit(HomeUiEvent.ShowToast(UiText.DirectString(exception.message.toString())))
Timber.e(exception.message)
}
}
_uiState.value = _uiState.value.copy(isNetworking = false)
}
}

Expand Down

0 comments on commit 34f2a47

Please sign in to comment.