Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 리액션 모아서 보내기 #246

Merged
merged 10 commits into from
Oct 3, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package team.ppac.common.kotlin.model

import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger

class ReactionState {
private val _isFirstClickEvent = AtomicBoolean(true)
private val _reactionCount = AtomicInteger(0)
private val _isUpdating = AtomicBoolean(false)
Comment on lines +7 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

각각 타입들이 좀 특이한거같은데 설명 한번 부탁함당

Copy link
Collaborator Author

@hj1115hj hj1115hj Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private val _isFirstClickEvent = AtomicBoolean(true)

  • 맨 처음 ㅋㅋ버튼 클릭했을했는지 확인 하는 변수
  • 처음 viewModel 진입시는 true

private val _reactionCount = AtomicInteger(0)

  • 400ms 안에 연속으로 클리된 count횟수

private val _isUpdating = AtomicBoolean(false):

  • 서버가 업로드 하는동안 reactionCount 증가되는거 막기위해서 업로드 중인지 비교하는 변수
  • reactionCount가 증가되면 나중에 서버 실패시에 증가시킨 만큼 빼야되는데 그게 불일치할까마 막았음!

Atomic한이유는 코루틴을 병렬로 실행하면서 저기 있는 값들을 변경하는데 동시에 변경하려고 할때 안정성을 보장하려고 다른 코루틴이 읽거나 쓸때 접근하지 못하도록 lock건거입니다!
저거 안하면 병렬 타이밍 이슈로 클릭횟수보다 counting된게 적거나 많고 그러더라구욧

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

신기방기하구만유

private var lastClickTime: Long = 0

val reactionCount: Int
get() = _reactionCount.get()

val isUpdating: Boolean
get() = _isUpdating.get()

val isFirstClickEvent: Boolean
get() = _isFirstClickEvent.get()

fun setIsFirstClickEvent(value: Boolean) {
_isFirstClickEvent.set(value)
}

fun addReactionCount(count: Int) {
_reactionCount.addAndGet(count)
}

fun startUpdate() {
_isUpdating.compareAndSet(false, true)
}

fun endUpdate() {
_isUpdating.set(false)
}

fun releaseState() {
_reactionCount.set(0)
_isFirstClickEvent.set(true)
}

fun isDoubleClickEvent(): Boolean {
val currentClickTime: Long = System.currentTimeMillis()
return if (currentClickTime - lastClickTime <= DOUBLE_CLICK_INTERVAL) {
true
} else {
lastClickTime = System.currentTimeMillis()
false
}
}

companion object {
private const val DOUBLE_CLICK_INTERVAL = 400
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package team.ppac.data.mapper

import team.ppac.domain.model.ReactionMeme
import team.ppac.remote.model.response.meme.ReactionMemeResponse

internal fun ReactionMemeResponse.toReactionMeme(): ReactionMeme = ReactionMeme(
count = count,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package team.ppac.data.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import team.ppac.data.mapper.toMeme
import team.ppac.data.mapper.toReactionMeme
import team.ppac.data.paging.ITEMS_PER_PAGE
import team.ppac.data.paging.createPager
import team.ppac.domain.model.Meme
import team.ppac.domain.model.MemeWatchType
import team.ppac.domain.model.MemeWithPagination
import team.ppac.domain.model.ReactionMeme
import team.ppac.domain.repository.MemeRepository
import team.ppac.domain.repository.SavedMemeEvent
import team.ppac.remote.datasource.MemeDataSource
Expand Down Expand Up @@ -58,8 +60,8 @@ internal class MemeRepositoryImpl @Inject constructor(
)
}

override suspend fun reactMeme(memeId: String): Boolean {
return memeDataSource.reactMeme(memeId)
override suspend fun reactMeme(memeId: String, count: Int): ReactionMeme {
return memeDataSource.reactMeme(memeId, count).toReactionMeme()
}

override suspend fun watchMeme(memeId: String, watchType: MemeWatchType): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ fun FarmemeWeakButton(
text: String = "",
textColor: Color = FarmemeTheme.textColor.primary,
withStar: Boolean = false,
isDebounceClick: Boolean = true,
onClick: () -> Unit = { },
icon: @Composable () -> Unit,
) {
Row(
modifier = modifier
.clip(FarmemeRadius.Radius25.shape)
.background(color = backgroundColor)
.noRippleClickable(onClick = onClick)
.noRippleClickable(onClick = onClick, isDebounceClick = isDebounceClick)
.padding(15.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import team.ppac.common.kotlin.model.MultipleEventsCutter
@SuppressLint("UnnecessaryComposedModifier")
fun Modifier.noRippleClickable(
enabled: Boolean = true,
isDebounceClick: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit,
onClick: () -> Unit
): Modifier = composed {
this then singleClickable(
indication = null,
enabled = enabled,
isDebounceClick = isDebounceClick,
onClickLabel = onClickLabel,
role = role,
onClick = onClick
Expand All @@ -31,37 +33,43 @@ fun Modifier.noRippleClickable(
@SuppressLint("UnnecessaryComposedModifier")
fun Modifier.rippleClickable(
rippleColor: Color = Color.Unspecified,
isDebounceClick: Boolean = true,
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit,
onClick: () -> Unit
): Modifier = composed {
this then singleClickable(
indication = rememberRipple(color = rippleColor),
enabled = enabled,
isDebounceClick = isDebounceClick,
onClickLabel = onClickLabel,
role = role,
onClick = onClick
onClick = onClick,
)
}

@SuppressLint("ModifierFactoryUnreferencedReceiver")
private fun Modifier.singleClickable(
indication: Indication?,
enabled: Boolean = true,
debounceMillis: Long = 300L,
isDebounceClick: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
debounceMillis: Long = 300L,
onClick: () -> Unit,
): Modifier = composed {
val multipleEventsCutter = remember { MultipleEventsCutter(debounceMillis) }

clickable(
interactionSource = remember { MutableInteractionSource() },
indication = indication,
enabled = enabled,
onClickLabel = onClickLabel,
role = role,
onClick = { multipleEventsCutter.processEvent(onClick) },
onClick = if (isDebounceClick) {
{ multipleEventsCutter.processEvent(onClick) }
} else {
{ onClick() }
},
Comment on lines -65 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

singleClickable 모디파이어가 디바운스를 의도하고 만들어진 모디파이어인데 isDebounceClick이 추가된 이유는 무엇인가여

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋ버튼 연타할려면 디바운스 없어야해가지구 isDebounceClick 이 false면 디바운스 미적용 true면 적요으로 할려고 했숩니다! 기존에 있는 Clickable extension이랑 함께 쓸려고 flag로 구분함!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아예 그럼 별도로 디바운스가 없는 단순 clickable을 만드는게 더 좋아보여용, 아니면 기본 clickable을 사용하던가??
singleClickable에 isDebounceClick과 같이 디바운스 자체를 제어하는 파라미터는 있으면 안될 것 같은 개인적 생각..?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오께이 리팩토링으로 잡겠습니다

)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package team.ppac.domain.model

data class ReactionMeme (
val count: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow
import team.ppac.domain.model.Meme
import team.ppac.domain.model.MemeWatchType
import team.ppac.domain.model.MemeWithPagination
import team.ppac.domain.model.ReactionMeme

interface MemeRepository {
suspend fun getMeme(memeId: String): Meme
Expand All @@ -14,8 +15,7 @@ interface MemeRepository {
keyword: String,
getCurrentPage: (Int) -> Unit
): MemeWithPagination

suspend fun reactMeme(memeId: String): Boolean
suspend fun reactMeme(memeId: String, count: Int): ReactionMeme
suspend fun watchMeme(
memeId: String,
watchType: MemeWatchType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package team.ppac.domain.usecase

import team.ppac.domain.model.ReactionMeme
import team.ppac.domain.repository.MemeRepository
import javax.inject.Inject

interface ReactMemeUseCase {
suspend operator fun invoke(memeId: String): Boolean
suspend operator fun invoke(memeId: String, count: Int): ReactionMeme
}

internal class ReactMemeUseCaseImpl @Inject constructor(
private val memeRepository: MemeRepository,
) : ReactMemeUseCase {
override suspend fun invoke(memeId: String): Boolean {
return memeRepository.reactMeme(memeId)
override suspend fun invoke(memeId: String, count: Int): ReactionMeme {
return memeRepository.reactMeme(memeId, count)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package team.ppac.remote.api

import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
import team.ppac.remote.model.request.meme.ReactMemeRequest
import team.ppac.remote.model.response.meme.MemeResponse
import team.ppac.remote.model.response.meme.ReactionMemeResponse
import team.ppac.remote.model.response.user.MemesResponse
import team.ppac.remote.model.response.meme.UploadMemeResponse

Expand Down Expand Up @@ -37,7 +40,10 @@ internal interface MemeApi {
): MemesResponse

@POST("/api/meme/{memeId}/reaction")
suspend fun reactMeme(@Path("memeId") memeId: String): Boolean
suspend fun reactMeme(
@Path("memeId") memeId: String,
@Body reactMemeRequest: ReactMemeRequest
): ReactionMemeResponse

@POST("/api/meme/{memeId}/watch/{type}")
suspend fun watchMeme(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package team.ppac.remote.datasource

import team.ppac.remote.model.response.meme.MemeResponse
import team.ppac.remote.model.response.meme.ReactionMemeResponse
import team.ppac.remote.model.response.user.MemesResponse

interface MemeDataSource {
Expand All @@ -13,7 +14,7 @@ interface MemeDataSource {
page: Int,
size: Int,
): MemesResponse
suspend fun reactMeme(memeId: String): Boolean
suspend fun reactMeme(memeId: String, count: Int): ReactionMemeResponse
suspend fun watchMeme(
memeId: String,
type: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import team.ppac.remote.api.MemeApi
import team.ppac.remote.datasource.MemeDataSource
import team.ppac.remote.model.request.meme.ReactMemeRequest
import team.ppac.remote.model.response.meme.MemeResponse
import team.ppac.remote.model.response.meme.ReactionMemeResponse
import team.ppac.remote.model.response.user.MemesResponse
import java.io.File
import java.io.FileOutputStream
Expand Down Expand Up @@ -43,8 +45,8 @@ internal class MemeDataSourceImpl @Inject constructor(
return memeApi.getSearchMemes(keyword, page, size)
}

override suspend fun reactMeme(memeId: String): Boolean {
return memeApi.reactMeme(memeId)
override suspend fun reactMeme(memeId: String, count: Int): ReactionMemeResponse {
return memeApi.reactMeme(memeId, reactMemeRequest = ReactMemeRequest(count))
}

override suspend fun watchMeme(memeId: String, type: String): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.ppac.remote.model.request.meme

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ReactMemeRequest (
@field:Json(name = "count")
val count: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.ppac.remote.model.response.meme

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ReactionMemeResponse (
@field:Json(name = "count")
val count: Int,
)
Loading