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

#308 [feat] 대표 룰 수정 api 및 presentation 로직 #310

Merged
merged 10 commits into from
Sep 9, 2023
11 changes: 11 additions & 0 deletions app/src/main/java/hous/release/android/di/ReducerModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import hous.release.android.presentation.our_rules.event.AddRuleReducer
import hous.release.android.presentation.our_rules.event.MainRuleReducer
import hous.release.android.presentation.our_rules.event.RepresentRulesReducer
import hous.release.android.presentation.our_rules.event.UpdateRuleReducer
import hous.release.android.presentation.our_rules.viewmodel.AddRuleEvent
import hous.release.android.presentation.our_rules.viewmodel.AddRuleState
import hous.release.android.presentation.our_rules.viewmodel.MainRulesEvent
import hous.release.android.presentation.our_rules.viewmodel.MainRulesState
import hous.release.android.presentation.our_rules.viewmodel.RepresentRulesEvent
import hous.release.android.presentation.our_rules.viewmodel.RepresentRulesState
import hous.release.android.presentation.our_rules.viewmodel.UpdateRuleEvent
import hous.release.android.presentation.our_rules.viewmodel.UpdateRuleState
import hous.release.android.util.event.Reducer
Expand All @@ -30,8 +33,16 @@ abstract class ReducerModule {
@UpdateRule
@Binds
abstract fun bindUpdateRuleReducer(updateRuleReducer: UpdateRuleReducer): Reducer<UpdateRuleState, UpdateRuleEvent>

@RepresentRules
@Binds
abstract fun bindRepresentRulesReducer(representRulesReducer: RepresentRulesReducer): Reducer<RepresentRulesState, RepresentRulesEvent>
}

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class RepresentRules

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainRules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,31 @@ private fun MainRuleContentPreview() {
}
}

@Preview(name = "main rule Item", showBackground = true)
@Preview(name = "general rule Item", showBackground = true)
@Composable
private fun MainRulePreview() {
private fun Preview1() {
HousTheme {
Surface {
MainRuleItem()
}
}
}

@Preview(name = "new main rule Item", showBackground = true)
@Preview(name = "Represent rule Item", showBackground = true)
@Composable
private fun NewMainRulePreview() {
private fun Preview2() {
HousTheme {
Surface {
MainRuleItem(
mainRule = Rule().copy(isRepresent = true)
)
}
}
}

@Preview(name = "new rule Item", showBackground = true)
@Composable
private fun Preview3() {
HousTheme {
Surface {
MainRuleItem(
Expand All @@ -118,3 +130,15 @@ private fun NewMainRulePreview() {
}
}
}

@Preview(name = "Represent and New rule Item", showBackground = true)
@Composable
private fun Preview4() {
HousTheme {
Surface {
MainRuleItem(
mainRule = Rule().copy(isNew = true, isRepresent = true)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ fun PhotoItem(

private fun Modifier.deleteButtonLayout() = this.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val placeableY = (placeable.height * 0.38f).toInt()
val placeableX = (placeable.width * 0.38f).toInt()
val placeableY = (placeable.height * 0.36f).toInt()
val placeableX = (placeable.width * 0.36f).toInt()
layout(placeable.width, placeable.height) {
placeable.place(
x = placeableX,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package hous.release.android.presentation.our_rules.event

import hous.release.android.presentation.our_rules.model.RepresentRuleUiModel
import hous.release.android.presentation.our_rules.viewmodel.RepresentRulesEvent
import hous.release.android.presentation.our_rules.viewmodel.RepresentRulesState
import hous.release.android.util.event.Reducer
import javax.inject.Inject

class RepresentRulesReducer @Inject constructor() :
Reducer<RepresentRulesState, RepresentRulesEvent> {
override fun dispatch(
state: RepresentRulesState,
event: RepresentRulesEvent
): RepresentRulesState {
return when (event) {
is RepresentRulesEvent.FetchRules -> {
state.copy(
originRules = event.rules.map { RepresentRuleUiModel.from(it) },
rules = event.rules.map { RepresentRuleUiModel.from(it) }
)
}

is RepresentRulesEvent.UpdateRule -> {
state.copy(
rules = state.rules.map { rule ->
if (rule.id == event.id) rule.copy(isRepresent = rule.isRepresent.not())
else rule
}
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ import hous.release.android.presentation.our_rules.component.dialog.UpdateRuleOu
import hous.release.android.presentation.our_rules.model.DetailRuleUiModel
import hous.release.android.presentation.our_rules.screen.AddRuleScreen
import hous.release.android.presentation.our_rules.screen.MainRuleScreen
import hous.release.android.presentation.our_rules.screen.RepresentRuleScreen
import hous.release.android.presentation.our_rules.screen.UpdateRuleScreen
import hous.release.android.presentation.our_rules.screen.type.RulesScreens
import hous.release.android.presentation.our_rules.viewmodel.AddRuleSideEffect
import hous.release.android.presentation.our_rules.viewmodel.AddRuleViewModel
import hous.release.android.presentation.our_rules.viewmodel.MainRuleSideEffect
import hous.release.android.presentation.our_rules.viewmodel.MainRuleViewModel
import hous.release.android.presentation.our_rules.viewmodel.RepresentRuleViewModel
import hous.release.android.presentation.our_rules.viewmodel.RepresentRulesSideEffect
import hous.release.android.presentation.our_rules.viewmodel.UpdateRuleSideEffect
import hous.release.android.presentation.our_rules.viewmodel.UpdateRuleViewModel
import hous.release.android.presentation.practice.findActivity
Expand Down Expand Up @@ -105,7 +108,7 @@ private fun NavGraphBuilder.mainRuleScreen(
onNavigateToRepresentRule = navController::navigateToRepresentRule,
onFinish = activity::finish,
refresh = viewModel::fetchMainRules,
deleteRule = viewModel::deleteRule,
deleteRule = viewModel::deleteRule
)
}
}
Expand Down Expand Up @@ -305,10 +308,70 @@ private fun NavGraphBuilder.addRuleScreen(onBack: () -> Unit) {
}
}

@OptIn(ExperimentalLifecycleComposeApi::class)
private fun NavGraphBuilder.representativeRuleScreen(onBack: () -> Unit) {
composable(RulesScreens.Represent.route) {
val viewModel = hiltViewModel<RepresentRuleViewModel>()
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
var isLoading by remember { mutableStateOf(false) }
var isOutDialogShow by remember { mutableStateOf(false) }

val context = LocalContext.current

LaunchedEffect(Unit) {
viewModel.sideEffect.collect { event ->
when (event) {
is RepresentRulesSideEffect.IDLE -> Unit

is RepresentRulesSideEffect.ShowLimitRulesToast -> {
ToastMessageUtil.showToast(
context,
context.getString(R.string.our_rule_limit_represent_rules_count)
)
}

is RepresentRulesSideEffect.LoadingBar -> {
isLoading = event.isLoading
}

is RepresentRulesSideEffect.PopBackStack -> {
onBack()
}
}
}
}

val onBackPressed = {
if (viewModel.isSavable) {
isOutDialogShow = true
} else {
onBack()
}
}

BackHandler(viewModel.isSavable, onBackPressed)
if (isLoading) LoadingBar()
if (isOutDialogShow) {
UpdateRuleOutDialog(
onConfirm = {
isOutDialogShow = false
onBack()
},
onDismiss = {
isOutDialogShow = false
}
)
}
RepresentRuleScreen(
rules = uiState.value.rules,
onBack = onBackPressed,
onUpdateRule = viewModel::updateRuleBy,
isSavable = viewModel::isSavable,
onSave = viewModel::saveRules
)
}
}

// Navigation
fun NavController.navigateToAddRule() {
navigate(RulesScreens.Add.route)
Expand All @@ -324,4 +387,4 @@ fun NavController.navigateUpdateRule(detailRuleUiModel: DetailRuleUiModel) {

fun NavController.navigateToRepresentRule() {
navigate(RulesScreens.Represent.route)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,26 @@ import hous.release.domain.entity.rule.Rule
@Composable
fun RepresentRuleScreen(
rules: List<RepresentRuleUiModel> = emptyList(),
isChanged: Boolean = false,
isSavable: () -> Boolean = { false },
onSave: () -> Unit = {},
onBack: () -> Unit = {},
onRuleClick: (Int) -> Unit = {}
onUpdateRule: (Int) -> Unit = {}
) {
Column(
modifier = Modifier.fillMaxSize()
) {
RuleToolbar(
title = stringResource(id = R.string.our_rule_represent_rule_title),
trailingTitle = stringResource(id = R.string.our_rule_save_new_rule),
isButtonActive = isChanged,
isButtonActive = isSavable(),
onBack = onBack,
onAddButton = onSave
)
Spacer(modifier = Modifier.height(20.dp))
RepresentRuleList(
modifier = Modifier.padding(horizontal = 16.dp),
rules = rules,
onClick = onRuleClick
onClick = onUpdateRule
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package hous.release.android.presentation.our_rules.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hous.release.android.di.RepresentRules
import hous.release.android.presentation.our_rules.model.RepresentRuleUiModel
import hous.release.android.util.event.Reducer
import hous.release.domain.entity.rule.Rule
import hous.release.domain.usecase.rule.GetRulesUseCase
import hous.release.domain.usecase.rule.UpdateRepresentRulesUseCase
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import retrofit2.HttpException
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
class RepresentRuleViewModel @Inject constructor(
private val updateRepresentRulesUseCase: UpdateRepresentRulesUseCase,
private val getRulesUseCase: GetRulesUseCase,
@RepresentRules private val reducer: Reducer<RepresentRulesState, RepresentRulesEvent>
) : ViewModel() {

private val uiEvents = Channel<RepresentRulesEvent>()
val uiState: StateFlow<RepresentRulesState> = uiEvents
.receiveAsFlow()
.runningFold(RepresentRulesState(), reducer::dispatch)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), RepresentRulesState())

private val _sideEffect: Channel<RepresentRulesSideEffect> = Channel()
val sideEffect = _sideEffect.receiveAsFlow()

init {
viewModelScope.launch {
val rules = getRulesUseCase()
uiEvents.send(RepresentRulesEvent.FetchRules(rules))
}
}

val isSavable get() = uiState.value.rules != uiState.value.originRules

fun updateRuleBy(id: Int) {
viewModelScope.launch {
if (canUpdateRuleBy(id)) {
uiEvents.send(RepresentRulesEvent.UpdateRule(id))
return@launch
}
_sideEffect.send(RepresentRulesSideEffect.ShowLimitRulesToast)
}
}

fun saveRules() {
viewModelScope.launch {
_sideEffect.send(RepresentRulesSideEffect.LoadingBar(true))
runCatching {
updateRepresentRulesUseCase(
uiState.value.rules.filter { it.isRepresent }
.map { it.id }
)
}.onSuccess {
_sideEffect.send(RepresentRulesSideEffect.LoadingBar(false))
_sideEffect.send(RepresentRulesSideEffect.PopBackStack)
}.onFailure { e ->
Timber.e("updateRule() - onFailure() - e: ${e.stackTraceToString()}")
if (e is HttpException) {
when (e.code()) {
LIMIT_RULES_CODE -> _sideEffect.send(RepresentRulesSideEffect.ShowLimitRulesToast)
}
}
_sideEffect.send(RepresentRulesSideEffect.LoadingBar(false))
}
}
}

private fun canUpdateRuleBy(id: Int): Boolean {
val isRepresent = uiState.value.rules.find { it.id == id }?.isRepresent ?: false
if (isRepresent) return true
return uiState.value.rules.count { it.isRepresent } + 1 <= 3
}

private companion object {
const val LIMIT_RULES_CODE = 403
}
}

sealed class RepresentRulesSideEffect {
object IDLE : RepresentRulesSideEffect()
data class LoadingBar(val isLoading: Boolean) : RepresentRulesSideEffect()
object ShowLimitRulesToast : RepresentRulesSideEffect()
object PopBackStack : RepresentRulesSideEffect()
}

data class RepresentRulesState(
val originRules: List<RepresentRuleUiModel> = emptyList(),
val rules: List<RepresentRuleUiModel> = emptyList()
)

sealed class RepresentRulesEvent {
data class FetchRules(val rules: List<Rule>) : RepresentRulesEvent()
data class UpdateRule(val id: Int) : RepresentRulesEvent()
}
5 changes: 3 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="http://schemas.android.com/tools">
<string name="app_name">Hous-</string>

<!-- Util -->
Expand Down Expand Up @@ -106,7 +106,8 @@
<string name="hous_empty_our_rules">아직 우리 집 Rules가 없어요!</string>
<string name="hous_empty_our_rules_add">다른 Rule도 추가해보세요!</string>
<string name="our_rule_duplicate_rule">똑같은 이름의 Rule이 있어요!</string>
<string name="our_rule_limit_photo_count">사진은 5개까지만 추가할 수 있어요!</string>
<string name="our_rule_limit_photo_count">사진은 5개 까지만 추가할 수 있어요!</string>
<string name="our_rule_limit_represent_rules_count">대표 rule 은 3개 까지만 추가할 수 있어요!</string>
<string name="our_rule_limit_title">Rules 개수 초과</string>
<string name="our_rule_limit_content">우리 집 rule이 너무 많아요!\n필요하지 않은 rule을 삭제하고\n다시 시도해주세요~</string>
<string name="our_rule_empty_description">아직 우리 집 Rules가 없어요!</string>
Expand Down
Loading
Loading