From b3a05d669153cead552d09f3f293189e1acfa5ca Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Tue, 11 Nov 2025 21:45:06 +0900 Subject: [PATCH 01/13] =?UTF-8?q?Refactor:=20EmotionViewModel=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20MviViewModel=20=EA=B5=AC=ED=98=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20orbit=20ContainerHost=20=EC=A7=81?= =?UTF-8?q?=EC=A0=91=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/emotion/EmotionScreen.kt | 8 +- .../presentation/emotion/EmotionViewModel.kt | 208 +++++++----------- .../emotion/model/mvi/EmotionIntent.kt | 18 -- .../emotion/model/mvi/EmotionSideEffect.kt | 4 +- .../emotion/model/mvi/EmotionState.kt | 4 +- 5 files changed, 89 insertions(+), 153 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt index c8e09327..f3e64706 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt @@ -4,33 +4,33 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.presentation.common.dimension.pxToDp -import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.common.toast.GlobalBitnagilToast import com.threegap.bitnagil.presentation.emotion.component.template.EmotionRecommendRoutineScreen import com.threegap.bitnagil.presentation.emotion.component.template.SimpleEmotionSelectionScreen import com.threegap.bitnagil.presentation.emotion.component.template.SwipeEmotionSelectionScreen import com.threegap.bitnagil.presentation.emotion.model.EmotionScreenStep import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionSideEffect +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun EmotionScreenContainer( viewModel: EmotionViewModel = hiltViewModel(), navigateToBack: () -> Unit, ) { - val state by viewModel.stateFlow.collectAsState() + val state by viewModel.collectAsState() BackHandler { viewModel.moveToPrev() } - viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { EmotionSideEffect.NavigateToBack -> navigateToBack() is EmotionSideEffect.ShowToast -> GlobalBitnagilToast.showWarning(sideEffect.message) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt index 8bc3a3cb..042607e1 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt @@ -1,179 +1,135 @@ package com.threegap.bitnagil.presentation.emotion import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionsUseCase import com.threegap.bitnagil.domain.emotion.usecase.RegisterEmotionUseCase import com.threegap.bitnagil.domain.onboarding.usecase.RegisterRecommendOnBoardingRoutinesUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.emotion.model.EmotionRecommendRoutineUiModel import com.threegap.bitnagil.presentation.emotion.model.EmotionScreenStep import com.threegap.bitnagil.presentation.emotion.model.EmotionUiModel -import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionIntent import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionSideEffect import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class EmotionViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, private val getEmotionsUseCase: GetEmotionsUseCase, private val registerEmotionUseCase: RegisterEmotionUseCase, private val registerRecommendOnBoardingRoutinesUseCase: RegisterRecommendOnBoardingRoutinesUseCase, -) : MviViewModel( - savedStateHandle = savedStateHandle, - initState = EmotionState.Init, -) { + savedStateHandle: SavedStateHandle, +) : ContainerHost, ViewModel() { + + override val container: Container = container(initialState = EmotionState.Init, savedStateHandle = savedStateHandle) + init { loadEmotions() } - private fun loadEmotions() { - viewModelScope.launch { + private fun loadEmotions() = + intent { getEmotionsUseCase().fold( onSuccess = { emotions -> - sendIntent( - EmotionIntent.EmotionListLoadSuccess(emotionTypeUiModels = emotions.map { EmotionUiModel.fromDomain(it) }), - ) + reduce { + state.copy( + emotionTypeUiModels = emotions.map { EmotionUiModel.fromDomain(it) }, + isLoading = false, + ) + } }, onFailure = { // todo 실패 케이스 정의되면 처리 }, ) } - } - override suspend fun Syntax.reduceState(intent: EmotionIntent, state: EmotionState): EmotionState? { - when (intent) { - is EmotionIntent.EmotionListLoadSuccess -> { - return state.copy( - emotionTypeUiModels = intent.emotionTypeUiModels, - isLoading = false, - ) - } - is EmotionIntent.RegisterEmotionSuccess -> { - return state.copy( - recommendRoutines = intent.recommendRoutines, - step = EmotionScreenStep.RecommendRoutines, - isLoading = false, - showLoadingView = false, - ) - } - EmotionIntent.RegisterEmotionLoading -> { - return state.copy( + fun selectEmotion(emotionType: String, minimumDelay: Long = 0) = + intent { + val isLoading = state.isLoading + if (isLoading) return@intent + + reduce { + state.copy( isLoading = true, showLoadingView = true, ) } - EmotionIntent.RegisterRecommendRoutinesLoading -> { - return state.copy( - isLoading = true, - ) - } - EmotionIntent.RegisterRecommendRoutinesFailure -> { - return state.copy( - isLoading = false, - ) - } - EmotionIntent.RegisterRecommendRoutinesSuccess -> { - sendSideEffect(EmotionSideEffect.NavigateToBack) - return null - } - EmotionIntent.BackToSelectEmotionStep -> { - return state.copy( - recommendRoutines = listOf(), - step = EmotionScreenStep.Emotion, - isLoading = false, - ) - } - is EmotionIntent.SelectRecommendRoutine -> { - val selectChangedRecommendRoutines = state.recommendRoutines.map { - if (it.id == intent.recommendRoutineId) { - it.copy(selected = !it.selected) - } else { - it - } + viewModelScope.launch { + if (minimumDelay > 0) { + delay(minimumDelay) } - return state.copy(recommendRoutines = selectChangedRecommendRoutines) - } - - EmotionIntent.NavigateToBack -> { - sendSideEffect(EmotionSideEffect.NavigateToBack) - return null - } - is EmotionIntent.RegisterEmotionFailure -> { - sendSideEffect(EmotionSideEffect.ShowToast(intent.message)) - sendSideEffect(EmotionSideEffect.NavigateToBack) - return null + registerEmotionUseCase(emotionType = emotionType).fold( + onSuccess = { emotionRecommendRoutines -> + val recommendRoutines = emotionRecommendRoutines.map { EmotionRecommendRoutineUiModel.fromEmotionRecommendRoutine(it) } + reduce { + state.copy( + recommendRoutines = recommendRoutines, + step = EmotionScreenStep.RecommendRoutines, + isLoading = false, + showLoadingView = false, + ) + } + }, + onFailure = { + postSideEffect(EmotionSideEffect.ShowToast(message = it.message ?: "에러가 발생했습니다. 잠시 후 시도해주세요.")) + postSideEffect(EmotionSideEffect.NavigateToBack) + }, + ) } } - } - fun selectEmotion(emotionType: String, minimumDelay: Long = 0) { - val isLoading = stateFlow.value.isLoading - if (isLoading) return - - viewModelScope.launch { - sendIntent(EmotionIntent.RegisterEmotionLoading) - - if (minimumDelay > 0) { - delay(minimumDelay) + fun selectRecommendRoutine(recommendRoutineId: String) = + intent { + val selectChangedRecommendRoutines = state.recommendRoutines.map { + if (it.id == recommendRoutineId) { + it.copy(selected = !it.selected) + } else { + it + } } - - registerEmotionUseCase(emotionType = emotionType).fold( - onSuccess = { emotionRecommendRoutines -> - val recommendRoutines = emotionRecommendRoutines.map { EmotionRecommendRoutineUiModel.fromEmotionRecommendRoutine(it) } - sendIntent(EmotionIntent.RegisterEmotionSuccess(recommendRoutines)) - }, - onFailure = { - sendIntent( - EmotionIntent.RegisterEmotionFailure(message = it.message ?: "에러가 발생했습니다. 잠시 후 시도해주세요."), - ) - }, - ) - } - } - - fun selectRecommendRoutine(recommendRoutineId: String) { - viewModelScope.launch { - sendIntent(EmotionIntent.SelectRecommendRoutine(recommendRoutineId)) + reduce { state.copy(recommendRoutines = selectChangedRecommendRoutines) } } - } - fun moveToPrev() { - viewModelScope.launch { - val currentState = stateFlow.value - - when (currentState.step) { - EmotionScreenStep.Emotion -> sendIntent(EmotionIntent.NavigateToBack) - EmotionScreenStep.RecommendRoutines -> sendIntent(EmotionIntent.BackToSelectEmotionStep) + fun moveToPrev() = + intent { + when (state.step) { + EmotionScreenStep.Emotion -> postSideEffect(EmotionSideEffect.NavigateToBack) + EmotionScreenStep.RecommendRoutines -> reduce { + state.copy( + recommendRoutines = listOf(), + step = EmotionScreenStep.Emotion, + isLoading = false, + ) + } } } - } - fun registerRecommendRoutines() { - val isLoading = stateFlow.value.isLoading - if (isLoading) return - - viewModelScope.launch { - sendIntent(EmotionIntent.RegisterRecommendRoutinesLoading) - - val currentState = stateFlow.value - val selectedRecommendRoutineIds = currentState.recommendRoutines.filter { it.selected }.map { it.id } - registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds).fold( - onSuccess = { - sendIntent(EmotionIntent.RegisterRecommendRoutinesSuccess) - }, - onFailure = { - sendIntent(EmotionIntent.RegisterRecommendRoutinesFailure) - }, - ) + fun registerRecommendRoutines() = + intent { + val isLoading = state.isLoading + if (isLoading) return@intent + + viewModelScope.launch { + reduce { state.copy(isLoading = true) } + + val selectedRecommendRoutineIds = state.recommendRoutines.filter { it.selected }.map { it.id } + registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds).fold( + onSuccess = { + postSideEffect(EmotionSideEffect.NavigateToBack) + }, + onFailure = { + reduce { state.copy(isLoading = false) } + }, + ) + } } - } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt deleted file mode 100644 index 9e155caa..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.threegap.bitnagil.presentation.emotion.model.mvi - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent -import com.threegap.bitnagil.presentation.emotion.model.EmotionRecommendRoutineUiModel -import com.threegap.bitnagil.presentation.emotion.model.EmotionUiModel - -sealed class EmotionIntent : MviIntent { - data class EmotionListLoadSuccess(val emotionTypeUiModels: List) : EmotionIntent() - data class RegisterEmotionSuccess(val recommendRoutines: List) : EmotionIntent() - data object RegisterEmotionLoading : EmotionIntent() - data class RegisterEmotionFailure(val message: String) : EmotionIntent() - data object RegisterRecommendRoutinesLoading : EmotionIntent() - data object RegisterRecommendRoutinesSuccess : EmotionIntent() - data object RegisterRecommendRoutinesFailure : EmotionIntent() - data object BackToSelectEmotionStep : EmotionIntent() - data class SelectRecommendRoutine(val recommendRoutineId: String) : EmotionIntent() - data object NavigateToBack : EmotionIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionSideEffect.kt index b039ec89..ce643e75 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.emotion.model.mvi -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed class EmotionSideEffect : MviSideEffect { +sealed class EmotionSideEffect { data object NavigateToBack : EmotionSideEffect() data class ShowToast(val message: String) : EmotionSideEffect() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt index b095935f..b4607781 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.presentation.emotion.model.mvi +import android.os.Parcelable import androidx.compose.runtime.Immutable -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState import com.threegap.bitnagil.presentation.emotion.model.EmotionRecommendRoutineUiModel import com.threegap.bitnagil.presentation.emotion.model.EmotionScreenStep import com.threegap.bitnagil.presentation.emotion.model.EmotionUiModel @@ -15,7 +15,7 @@ data class EmotionState( val recommendRoutines: List, val step: EmotionScreenStep, val showLoadingView: Boolean, -) : MviState { +) : Parcelable { companion object { val Init = EmotionState( emotionTypeUiModels = emptyList(), From a697338836c1beab931bbbacdcea6ff066921e9d Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 12 Nov 2025 21:13:35 +0900 Subject: [PATCH 02/13] =?UTF-8?q?Refactor:=20OnBoardingViewModel=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20MviViewModel=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20orbit=20ContainerHost=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onboarding/OnBoardingScreen.kt | 8 +- .../onboarding/OnBoardingViewModel.kt | 477 ++++++++---------- .../onboarding/model/mvi/OnBoardingIntent.kt | 19 - .../model/mvi/OnBoardingSideEffect.kt | 4 +- .../onboarding/model/mvi/OnBoardingState.kt | 5 +- 5 files changed, 219 insertions(+), 294 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt index f55e36cf..b781d995 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt @@ -5,14 +5,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.component.block.BitnagilProgressTopBar -import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.common.toast.GlobalBitnagilToast import com.threegap.bitnagil.presentation.onboarding.component.template.OnBoardingAbstractTemplate import com.threegap.bitnagil.presentation.onboarding.component.template.OnBoardingIntroTemplate @@ -22,6 +20,8 @@ import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingPageInfo import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingSetType import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingSideEffect import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingState +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun OnBoardingScreenContainer( @@ -29,9 +29,9 @@ fun OnBoardingScreenContainer( navigateToHome: () -> Unit, navigateToBack: () -> Unit, ) { - val state by onBoardingViewModel.stateFlow.collectAsState() + val state by onBoardingViewModel.collectAsState() - onBoardingViewModel.sideEffectFlow.collectAsEffect { sideEffect -> + onBoardingViewModel.collectSideEffect { sideEffect -> when (sideEffect) { OnBoardingSideEffect.MoveToPreviousScreen -> { navigateToBack() diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt index 905e0ba7..7723bc1f 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt @@ -1,6 +1,7 @@ package com.threegap.bitnagil.presentation.onboarding import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingAbstractUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingsUseCase @@ -8,12 +9,10 @@ import com.threegap.bitnagil.domain.onboarding.usecase.GetRecommendOnBoardingRou import com.threegap.bitnagil.domain.onboarding.usecase.GetUserOnBoardingUseCase import com.threegap.bitnagil.domain.onboarding.usecase.RegisterRecommendOnBoardingRoutinesUseCase import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingAbstractTextItem import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingItem import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingPageInfo import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingSetType -import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingIntent import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingSideEffect import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingState import com.threegap.bitnagil.presentation.onboarding.model.navarg.OnBoardingScreenArg @@ -24,8 +23,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container @HiltViewModel(assistedFactory = OnBoardingViewModel.Factory::class) class OnBoardingViewModel @AssistedInject constructor( @@ -37,14 +37,16 @@ class OnBoardingViewModel @AssistedInject constructor( private val fetchUserProfileUseCase: FetchUserProfileUseCase, private val getUserOnBoardingUseCase: GetUserOnBoardingUseCase, @Assisted private val onBoardingArg: OnBoardingScreenArg, -) : MviViewModel( - initState = OnBoardingState.Loading, - savedStateHandle = savedStateHandle, -) { +) : ContainerHost, ViewModel() { @AssistedFactory interface Factory { fun create(onBoardingArg: OnBoardingScreenArg): OnBoardingViewModel } + override val container: Container = container( + savedStateHandle = savedStateHandle, + initialState = OnBoardingState.Loading, + ) + // 내부에 전체 온보딩 항목 저장 private val selectOnBoardingPageInfos = mutableListOf() private var existedOnBoardingAbstract: OnBoardingPageInfo.ExistedOnBoardingAbstract? = null @@ -68,259 +70,136 @@ class OnBoardingViewModel @AssistedInject constructor( } } - private fun loadIntro() { - viewModelScope.launch { - val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" - - sendIntent(OnBoardingIntent.LoadIntroSuccess(userName = userName)) + private fun loadIntro() = intent { + val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" + + reduce { + OnBoardingState.Idle( + nextButtonEnable = true, + currentOnBoardingPageInfo = OnBoardingPageInfo.Intro, + totalStep = 1, + currentStep = 0, + onBoardingSetType = OnBoardingSetType.NEW, + userName = userName, + ) } } - private fun loadUserOnBoarding() { - viewModelScope.launch { - val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" - val userOnBoarding = getUserOnBoardingUseCase().fold( - onSuccess = { it }, - onFailure = { - sendIntent(OnBoardingIntent.LoadUserOnBoardingFailure(message = it.message ?: "에러가 발생했습니다. 잠시 후 시도해주세요.")) - return@launch - }, - ) - - val onBoardingAbstract = getOnBoardingAbstractUseCase(selectedItemIdsWithOnBoardingId = userOnBoarding) - - val abstractPagePrefixText = onBoardingAbstract.prefix - val abstractTexts = onBoardingAbstract.abstractTexts.map { onBoardingAbstractText -> - onBoardingAbstractText.textItems.map { onBoardingAbstractTextItem -> - OnBoardingAbstractTextItem.fromOnBoardingAbstractTextItem(onBoardingAbstractTextItem) - } + private fun loadUserOnBoarding() = intent { + val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" + val userOnBoarding = getUserOnBoardingUseCase().fold( + onSuccess = { it }, + onFailure = { + postSideEffect(OnBoardingSideEffect.MoveToPreviousScreen) + postSideEffect(OnBoardingSideEffect.ShowToast(message = it.message ?: "에러가 발생했습니다. 잠시 후 시도해주세요.")) + return@intent + }, + ) + + val onBoardingAbstract = getOnBoardingAbstractUseCase(selectedItemIdsWithOnBoardingId = userOnBoarding) + + val abstractPagePrefixText = onBoardingAbstract.prefix + val abstractTexts = onBoardingAbstract.abstractTexts.map { onBoardingAbstractText -> + onBoardingAbstractText.textItems.map { onBoardingAbstractTextItem -> + OnBoardingAbstractTextItem.fromOnBoardingAbstractTextItem(onBoardingAbstractTextItem) } + } - sendIntent( - OnBoardingIntent.LoadUserOnBoardingSuccess( - onBoardingAbstract = OnBoardingPageInfo.ExistedOnBoardingAbstract( - prefix = abstractPagePrefixText, - abstractTexts = abstractTexts, - ), - userName = userName, - ), + val existedOnBoardingAbstract = OnBoardingPageInfo.ExistedOnBoardingAbstract( + prefix = abstractPagePrefixText, + abstractTexts = abstractTexts, + ) + this@OnBoardingViewModel.existedOnBoardingAbstract = existedOnBoardingAbstract + + reduce { + OnBoardingState.Idle( + nextButtonEnable = true, + currentOnBoardingPageInfo = existedOnBoardingAbstract, + totalStep = 1, + currentStep = 0, + onBoardingSetType = OnBoardingSetType.RESET, + userName = userName, ) } } - fun loadOnBoardingItems() { - viewModelScope.launch { - val onBoardings = getOnBoardingsUseCase() - val onBoardingPages = onBoardings.map { onBoarding -> - OnBoardingPageInfo.SelectOnBoarding.fromOnBoarding(onBoarding = onBoarding) - } - - sendIntent(intent = OnBoardingIntent.LoadOnBoardingSuccess(onBoardingPageInfos = onBoardingPages)) + fun loadOnBoardingItems() = intent { + val onBoardings = getOnBoardingsUseCase() + val onBoardingPages = onBoardings.map { onBoarding -> + OnBoardingPageInfo.SelectOnBoarding.fromOnBoarding(onBoarding = onBoarding) } - } - override suspend fun Syntax.reduceState( - intent: OnBoardingIntent, - state: OnBoardingState, - ): OnBoardingState? { - when (intent) { - is OnBoardingIntent.LoadUserOnBoardingSuccess -> { - existedOnBoardingAbstract = intent.onBoardingAbstract - - return OnBoardingState.Idle( - nextButtonEnable = true, - currentOnBoardingPageInfo = intent.onBoardingAbstract, - totalStep = 1, - currentStep = 0, - onBoardingSetType = OnBoardingSetType.RESET, - userName = intent.userName, - ) - } + val currentState = state + if (currentState !is OnBoardingState.Idle) return@intent - is OnBoardingIntent.LoadIntroSuccess -> { - return OnBoardingState.Idle( - nextButtonEnable = true, - currentOnBoardingPageInfo = OnBoardingPageInfo.Intro, - totalStep = 1, - currentStep = 0, - onBoardingSetType = OnBoardingSetType.NEW, - userName = intent.userName, - ) - } - - is OnBoardingIntent.LoadOnBoardingSuccess -> { - val currentState = state - if (currentState !is OnBoardingState.Idle) return null + selectOnBoardingPageInfos.clear() + selectOnBoardingPageInfos.addAll(onBoardingPages) - selectOnBoardingPageInfos.clear() - selectOnBoardingPageInfos.addAll(intent.onBoardingPageInfos) - - return currentState.copy( - nextButtonEnable = false, - currentOnBoardingPageInfo = intent.onBoardingPageInfos.first(), - totalStep = selectOnBoardingPageInfos.size + 2, - currentStep = 1, - ) - } - - is OnBoardingIntent.SelectItem -> { - val currentState = state - if (currentState !is OnBoardingState.Idle) return null + reduce { + currentState.copy( + nextButtonEnable = false, + currentOnBoardingPageInfo = onBoardingPages.first(), + totalStep = selectOnBoardingPageInfos.size + 2, + currentStep = 1, + ) + } + } - val currentPageInfo = currentState.currentOnBoardingPageInfo - if (currentPageInfo !is OnBoardingPageInfo.SelectOnBoarding) return null + fun selectItem(itemId: String) = intent { + val currentState = state + if (currentState !is OnBoardingState.Idle) return@intent - val selectChangedCurrentPageInfo = currentPageInfo.selectItem(itemId = intent.itemId) - selectOnBoardingPageInfos[currentState.currentStep - 1] = selectChangedCurrentPageInfo - return currentState.copy( - currentOnBoardingPageInfo = selectChangedCurrentPageInfo, - nextButtonEnable = selectChangedCurrentPageInfo.isItemSelected, - ) - } + val currentPageInfo = currentState.currentOnBoardingPageInfo + if (currentPageInfo !is OnBoardingPageInfo.SelectOnBoarding) return@intent - is OnBoardingIntent.SelectNext -> { - val currentState = state - if (currentState !is OnBoardingState.Idle) return null + val selectChangedCurrentPageInfo = currentPageInfo.selectItem(itemId = itemId) + selectOnBoardingPageInfos[currentState.currentStep - 1] = selectChangedCurrentPageInfo - val isLastPageOfSelectOnBoarding = currentState.currentStep >= selectOnBoardingPageInfos.size - if (isLastPageOfSelectOnBoarding) return null + reduce { + currentState.copy( + currentOnBoardingPageInfo = selectChangedCurrentPageInfo, + nextButtonEnable = selectChangedCurrentPageInfo.isItemSelected, + ) + } + } - val nextOnBoardingPageInfo = selectOnBoardingPageInfos[currentState.currentStep] - val nextButtonEnable = nextOnBoardingPageInfo.isItemSelected - return currentState.copy( - currentOnBoardingPageInfo = nextOnBoardingPageInfo, - nextButtonEnable = nextButtonEnable, - currentStep = currentState.currentStep + 1, - ) - } + fun selectNext() = intent { + val currentState = state + if (currentState !is OnBoardingState.Idle) return@intent - is OnBoardingIntent.SelectPrevious -> { - val currentState = state - if (currentState !is OnBoardingState.Idle || currentState.currentStep == 0) { - sendSideEffect(sideEffect = OnBoardingSideEffect.MoveToPreviousScreen) - return null - } + val isLastSelectOnBoarding = currentState.currentStep >= selectOnBoardingPageInfos.size + if (isLastSelectOnBoarding) { + val selectedItemIdsWithOnBoardingId = getSelectedOnBoardingItemIdsWithId(selectOnBoardingPageInfos) - if (currentState.currentStep == 1) { - return if (currentState.onBoardingSetType == OnBoardingSetType.RESET && existedOnBoardingAbstract != null) { - currentState.copy( - currentStep = 0, - currentOnBoardingPageInfo = existedOnBoardingAbstract!!, - nextButtonEnable = true, - ) - } else { - currentState.copy( - currentStep = 0, - currentOnBoardingPageInfo = OnBoardingPageInfo.Intro, - nextButtonEnable = true, - ) - } - } + val onBoardingAbstract = getOnBoardingAbstractUseCase(selectedItemIdsWithOnBoardingId = selectedItemIdsWithOnBoardingId) - val isSelectOnBoardingStep = currentState.currentStep <= selectOnBoardingPageInfos.size - if (isSelectOnBoardingStep) { - val previousOnBoardingPageInfo = selectOnBoardingPageInfos[currentState.currentStep - 2] - val nextButtonEnable = previousOnBoardingPageInfo.isItemSelected - return currentState.copy( - currentOnBoardingPageInfo = previousOnBoardingPageInfo, - nextButtonEnable = nextButtonEnable, - currentStep = currentState.currentStep - 1, - ) - } else { - val selectOnBoardingPageInfo = selectOnBoardingPageInfos.last() - val nextButtonEnable = selectOnBoardingPageInfo.isItemSelected - return currentState.copy( - currentOnBoardingPageInfo = selectOnBoardingPageInfo, - nextButtonEnable = nextButtonEnable, - currentStep = selectOnBoardingPageInfos.size, - ) + val abstractPagePrefixText = onBoardingAbstract.prefix + val abstractTexts = onBoardingAbstract.abstractTexts.map { onBoardingAbstractText -> + onBoardingAbstractText.textItems.map { onBoardingAbstractTextItem -> + OnBoardingAbstractTextItem.fromOnBoardingAbstractTextItem(onBoardingAbstractTextItem) } } - is OnBoardingIntent.LoadRecommendRoutinesSuccess -> { - val currentState = state - if (currentState !is OnBoardingState.Idle) return null - - val recommendRoutinePageInfo = OnBoardingPageInfo.RecommendRoutines(routines = intent.routines) - return currentState.copy( - currentOnBoardingPageInfo = recommendRoutinePageInfo, + reduce { + currentState.copy( + currentOnBoardingPageInfo = OnBoardingPageInfo.Abstract( + prefix = abstractPagePrefixText, + abstractTexts = abstractTexts, + ), currentStep = currentState.currentStep + 1, - nextButtonEnable = !currentState.onBoardingSetType.mustSelectRecommendRoutine, ) } + } else { + val nextOnBoardingPageInfo = selectOnBoardingPageInfos[currentState.currentStep] + val nextButtonEnable = nextOnBoardingPageInfo.isItemSelected - is OnBoardingIntent.SelectRoutine -> { - val currentState = state - if (currentState !is OnBoardingState.Idle) return null - - val currentPageInfo = currentState.currentOnBoardingPageInfo - if (currentPageInfo !is OnBoardingPageInfo.RecommendRoutines) return null - - val selectChangedCurrentPageInfo = currentPageInfo.selectItem(itemId = intent.routineId) - return currentState.copy( - currentOnBoardingPageInfo = selectChangedCurrentPageInfo, - nextButtonEnable = selectChangedCurrentPageInfo.isItemSelected, - ) - } - - OnBoardingIntent.NavigateToHome -> { - sendSideEffect(sideEffect = OnBoardingSideEffect.NavigateToHomeScreen) - return null - } - - is OnBoardingIntent.LoadOnBoardingAbstractSuccess -> { - val currentState = state - if (currentState !is OnBoardingState.Idle) return null - - return currentState.copy( - currentOnBoardingPageInfo = intent.onBoardingAbstract, + reduce { + currentState.copy( + currentOnBoardingPageInfo = nextOnBoardingPageInfo, + nextButtonEnable = nextButtonEnable, currentStep = currentState.currentStep + 1, ) } - - is OnBoardingIntent.LoadUserOnBoardingFailure -> { - sendSideEffect(sideEffect = OnBoardingSideEffect.MoveToPreviousScreen) - sendSideEffect(sideEffect = OnBoardingSideEffect.ShowToast(message = intent.message)) - return null - } - } - } - - fun selectItem(itemId: String) { - viewModelScope.launch { - sendIntent(intent = OnBoardingIntent.SelectItem(itemId = itemId)) - } - } - - fun selectNext() { - viewModelScope.launch { - val currentState = stateFlow.value - if (currentState !is OnBoardingState.Idle) return@launch - - val isLastSelectOnBoarding = currentState.currentStep >= selectOnBoardingPageInfos.size - if (isLastSelectOnBoarding) { - val selectedItemIdsWithOnBoardingId = getSelectedOnBoardingItemIdsWithId(selectOnBoardingPageInfos) - - val onBoardingAbstract = getOnBoardingAbstractUseCase(selectedItemIdsWithOnBoardingId = selectedItemIdsWithOnBoardingId) - - val abstractPagePrefixText = onBoardingAbstract.prefix - val abstractTexts = onBoardingAbstract.abstractTexts.map { onBoardingAbstractText -> - onBoardingAbstractText.textItems.map { onBoardingAbstractTextItem -> - OnBoardingAbstractTextItem.fromOnBoardingAbstractTextItem(onBoardingAbstractTextItem) - } - } - - sendIntent( - intent = OnBoardingIntent.LoadOnBoardingAbstractSuccess( - onBoardingAbstract = OnBoardingPageInfo.Abstract( - prefix = abstractPagePrefixText, - abstractTexts = abstractTexts, - ), - ), - ) - } else { - sendIntent(intent = OnBoardingIntent.SelectNext) - } } } @@ -336,9 +215,59 @@ class OnBoardingViewModel @AssistedInject constructor( } } - fun selectPrevious() { - viewModelScope.launch { - sendIntent(intent = OnBoardingIntent.SelectPrevious) + fun selectPrevious() = intent { + val currentState = state + if (currentState !is OnBoardingState.Idle || currentState.currentStep == 0) { + postSideEffect(sideEffect = OnBoardingSideEffect.MoveToPreviousScreen) + return@intent + } + + if (currentState.currentStep == 1) { + if (currentState.onBoardingSetType == OnBoardingSetType.RESET && existedOnBoardingAbstract != null) { + reduce { + currentState.copy( + currentStep = 0, + currentOnBoardingPageInfo = existedOnBoardingAbstract!!, + nextButtonEnable = true, + ) + } + return@intent + } else { + reduce { + currentState.copy( + currentStep = 0, + currentOnBoardingPageInfo = OnBoardingPageInfo.Intro, + nextButtonEnable = true, + ) + } + return@intent + } + } + + val isSelectOnBoardingStep = currentState.currentStep <= selectOnBoardingPageInfos.size + if (isSelectOnBoardingStep) { + val previousOnBoardingPageInfo = selectOnBoardingPageInfos[currentState.currentStep - 2] + val nextButtonEnable = previousOnBoardingPageInfo.isItemSelected + reduce { + currentState.copy( + currentOnBoardingPageInfo = previousOnBoardingPageInfo, + nextButtonEnable = nextButtonEnable, + currentStep = currentState.currentStep - 1, + ) + } + return@intent + } else { + val selectOnBoardingPageInfo = selectOnBoardingPageInfos.last() + val nextButtonEnable = selectOnBoardingPageInfo.isItemSelected + + reduce { + currentState.copy( + currentOnBoardingPageInfo = selectOnBoardingPageInfo, + nextButtonEnable = nextButtonEnable, + currentStep = selectOnBoardingPageInfos.size, + ) + } + return@intent } } @@ -361,12 +290,10 @@ class OnBoardingViewModel @AssistedInject constructor( getRecommendOnBoardingRoutinesUseCase(selectedItems).fold( onSuccess = { recommendRoutines -> if (isActive) { - sendIntent( - intent = OnBoardingIntent.LoadRecommendRoutinesSuccess( - routines = recommendRoutines.map { - OnBoardingItem.fromOnBoardingRecommendRoutine(it) - }, - ), + applyRecommendRoutines( + routines = recommendRoutines.map { + OnBoardingItem.fromOnBoardingRecommendRoutine(it) + }, ) } }, @@ -376,43 +303,63 @@ class OnBoardingViewModel @AssistedInject constructor( } } - fun cancelLoadRecommendRoutines() { - loadRecommendRoutinesJob?.cancel() - } + private fun applyRecommendRoutines(routines: List) = intent { + val currentState = state + if (currentState !is OnBoardingState.Idle) return@intent - fun selectRoutine(routineId: String) { - viewModelScope.launch { - sendIntent(intent = OnBoardingIntent.SelectRoutine(routineId = routineId)) + val recommendRoutinePageInfo = OnBoardingPageInfo.RecommendRoutines(routines = routines) + reduce { + currentState.copy( + currentOnBoardingPageInfo = recommendRoutinePageInfo, + currentStep = currentState.currentStep + 1, + nextButtonEnable = !currentState.onBoardingSetType.mustSelectRecommendRoutine, + ) } } - fun registerRecommendRoutines() { - viewModelScope.launch { - val currentState = stateFlow.value - if (currentState !is OnBoardingState.Idle) return@launch + fun cancelLoadRecommendRoutines() { + loadRecommendRoutinesJob?.cancel() + } - val currentPageInfo = currentState.currentOnBoardingPageInfo - if (currentPageInfo !is OnBoardingPageInfo.RecommendRoutines) return@launch + fun selectRoutine(routineId: String) = intent { + val currentState = state + if (currentState !is OnBoardingState.Idle) return@intent - val selectedRoutineIds = currentPageInfo.routines.filter { routineItem -> - routineItem.selectedIndex != null - }.map { - it.id - } + val currentPageInfo = currentState.currentOnBoardingPageInfo + if (currentPageInfo !is OnBoardingPageInfo.RecommendRoutines) return@intent - registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds = selectedRoutineIds).fold( - onSuccess = { _ -> - sendIntent(intent = OnBoardingIntent.NavigateToHome) - }, - onFailure = { - }, + val selectChangedCurrentPageInfo = currentPageInfo.selectItem(itemId = routineId) + reduce { + currentState.copy( + currentOnBoardingPageInfo = selectChangedCurrentPageInfo, + nextButtonEnable = selectChangedCurrentPageInfo.isItemSelected, ) } } - fun skipRegisterRecommendRoutines() { - viewModelScope.launch { - sendIntent(intent = OnBoardingIntent.NavigateToHome) + fun registerRecommendRoutines() = intent { + val currentState = state + if (currentState !is OnBoardingState.Idle) return@intent + + val currentPageInfo = currentState.currentOnBoardingPageInfo + if (currentPageInfo !is OnBoardingPageInfo.RecommendRoutines) return@intent + + val selectedRoutineIds = currentPageInfo.routines.filter { routineItem -> + routineItem.selectedIndex != null + }.map { + it.id } + + registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds = selectedRoutineIds).fold( + onSuccess = { _ -> + postSideEffect(sideEffect = OnBoardingSideEffect.NavigateToHomeScreen) + }, + onFailure = { + }, + ) + } + + fun skipRegisterRecommendRoutines() = intent { + postSideEffect(sideEffect = OnBoardingSideEffect.NavigateToHomeScreen) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingIntent.kt deleted file mode 100644 index 57c013eb..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingIntent.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.threegap.bitnagil.presentation.onboarding.model.mvi - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent -import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingItem -import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingPageInfo - -sealed class OnBoardingIntent : MviIntent { - data class LoadOnBoardingSuccess(val onBoardingPageInfos: List) : OnBoardingIntent() - data class LoadOnBoardingAbstractSuccess(val onBoardingAbstract: OnBoardingPageInfo.Abstract) : OnBoardingIntent() - data class LoadRecommendRoutinesSuccess(val routines: List) : OnBoardingIntent() - data class SelectItem(val itemId: String) : OnBoardingIntent() - data class SelectRoutine(val routineId: String) : OnBoardingIntent() - data object SelectNext : OnBoardingIntent() - data object SelectPrevious : OnBoardingIntent() - data object NavigateToHome : OnBoardingIntent() - data class LoadUserOnBoardingSuccess(val onBoardingAbstract: OnBoardingPageInfo.ExistedOnBoardingAbstract, val userName: String) : OnBoardingIntent() - data class LoadUserOnBoardingFailure(val message: String) : OnBoardingIntent() - data class LoadIntroSuccess(val userName: String) : OnBoardingIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt index aa16667f..e02b5efe 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.onboarding.model.mvi -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed class OnBoardingSideEffect : MviSideEffect { +sealed class OnBoardingSideEffect { data object MoveToPreviousScreen : OnBoardingSideEffect() data object NavigateToHomeScreen : OnBoardingSideEffect() data class ShowToast(val message: String) : OnBoardingSideEffect() diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt index b097e4d0..99932577 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt @@ -1,16 +1,15 @@ package com.threegap.bitnagil.presentation.onboarding.model.mvi import android.os.Parcelable -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingPageInfo import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingSetType import kotlinx.parcelize.Parcelize @Parcelize -sealed class OnBoardingState(val progress: Float) : Parcelable, MviState { +sealed class OnBoardingState(val progress: Float) : Parcelable { @Parcelize - object Loading : OnBoardingState(progress = 0f) + data object Loading : OnBoardingState(progress = 0f) @Parcelize data class Idle( From 659b2e55e35f3ad144ebad62b6eb5013103f1d0b Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 12 Nov 2025 21:18:24 +0900 Subject: [PATCH 03/13] =?UTF-8?q?Refactor:=20MyPageViewModel=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20MviViewModel=20=EA=B5=AC=ED=98=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20orbit=20ContainerHost=20=EC=A7=81?= =?UTF-8?q?=EC=A0=91=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/mypage/MyPageScreen.kt | 4 +- .../presentation/mypage/MyPageViewModel.kt | 58 ++++++------------- .../presentation/mypage/model/MyPageIntent.kt | 7 --- .../mypage/model/MyPageSideEffect.kt | 4 +- .../presentation/mypage/model/MyPageState.kt | 6 +- 5 files changed, 23 insertions(+), 56 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt index 3feabcac..24bba96c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,6 +29,7 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton import com.threegap.bitnagil.designsystem.component.block.BitnagilOptionButton import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar import com.threegap.bitnagil.presentation.mypage.model.MyPageState +import org.orbitmvi.orbit.compose.collectAsState @Composable fun MyPageScreenContainer( @@ -39,7 +39,7 @@ fun MyPageScreenContainer( navigateToQnA: () -> Unit, navigateToOnBoarding: () -> Unit, ) { - val state by myPageViewModel.stateFlow.collectAsState() + val state by myPageViewModel.collectAsState() MyPageScreen( state = state, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt index 3eb07818..d9b591d1 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageViewModel.kt @@ -1,57 +1,37 @@ package com.threegap.bitnagil.presentation.mypage -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.mypage.model.MyPageIntent import com.threegap.bitnagil.presentation.mypage.model.MyPageSideEffect import com.threegap.bitnagil.presentation.mypage.model.MyPageState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class MyPageViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, private val fetchUserProfileUseCase: FetchUserProfileUseCase, -) : MviViewModel( - MyPageState.Init, - savedStateHandle, -) { +) : ContainerHost, ViewModel() { + override val container: Container = container(initialState = MyPageState.Init) + init { loadMyPageInfo() } - private fun loadMyPageInfo() { - viewModelScope.launch { - fetchUserProfileUseCase().fold( - onSuccess = { - sendIntent( - MyPageIntent.LoadMyPageSuccess( - name = it.nickname, - profileUrl = "profileUrl", - ), + private fun loadMyPageInfo() = intent { + fetchUserProfileUseCase().fold( + onSuccess = { + reduce { + state.copy( + name = it.nickname, + profileUrl = "profileUrl", ) - }, - onFailure = { - }, - ) - } - } - - override suspend fun Syntax.reduceState( - intent: MyPageIntent, - state: MyPageState, - ): MyPageState { - when (intent) { - is MyPageIntent.LoadMyPageSuccess -> { - return state.copy( - name = intent.name, - profileUrl = intent.profileUrl, - ) - } - } + } + }, + onFailure = { + }, + ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt deleted file mode 100644 index 050d7762..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageIntent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.threegap.bitnagil.presentation.mypage.model - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent - -sealed class MyPageIntent : MviIntent { - data class LoadMyPageSuccess(val name: String, val profileUrl: String) : MyPageIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageSideEffect.kt index 9d7a1dde..a11e85c2 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageSideEffect.kt @@ -1,5 +1,3 @@ package com.threegap.bitnagil.presentation.mypage.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed class MyPageSideEffect : MviSideEffect +sealed class MyPageSideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt index 3fbec011..0c17af7c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/model/MyPageState.kt @@ -1,13 +1,9 @@ package com.threegap.bitnagil.presentation.mypage.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState -import kotlinx.parcelize.Parcelize - -@Parcelize data class MyPageState( val name: String, val profileUrl: String, -) : MviState { +) { companion object { val Init = MyPageState(name = "", profileUrl = "") } From 50adf58c0ca567bc4f5c3e233b459797c49e272b Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 12 Nov 2025 21:51:21 +0900 Subject: [PATCH 04/13] =?UTF-8?q?Refactor:=20WriteRoutineViewModel=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20MviViewModel=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20orbit=20ContainerHost=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../writeroutine/WriteRoutineScreen.kt | 8 +- .../writeroutine/WriteRoutineViewModel.kt | 646 ++++++++---------- .../model/mvi/WriteRoutineIntent.kt | 47 -- .../model/mvi/WriteRoutineSideEffect.kt | 4 +- .../model/mvi/WriteRoutineState.kt | 4 +- 5 files changed, 281 insertions(+), 428 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt index ba445a51..98741027 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,7 +27,6 @@ import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar -import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.common.toast.GlobalBitnagilToast import com.threegap.bitnagil.presentation.writeroutine.component.atom.namefield.NameField import com.threegap.bitnagil.presentation.writeroutine.component.atom.selectcell.SelectCell @@ -45,15 +43,17 @@ import com.threegap.bitnagil.presentation.writeroutine.model.Time import com.threegap.bitnagil.presentation.writeroutine.model.WriteRoutineType import com.threegap.bitnagil.presentation.writeroutine.model.mvi.WriteRoutineSideEffect import com.threegap.bitnagil.presentation.writeroutine.model.mvi.WriteRoutineState +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun WriteRoutineScreenContainer( viewModel: WriteRoutineViewModel = hiltViewModel(), navigateToBack: () -> Unit, ) { - val state by viewModel.stateFlow.collectAsState() + val state by viewModel.collectAsState() - viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { WriteRoutineSideEffect.MoveToPreviousScreen -> { navigateToBack() diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt index ad4227ba..63e9b56d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt @@ -1,14 +1,13 @@ package com.threegap.bitnagil.presentation.writeroutine import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.threegap.bitnagil.domain.recommendroutine.usecase.GetRecommendRoutineUseCase import com.threegap.bitnagil.domain.routine.usecase.GetRoutineUseCase import com.threegap.bitnagil.domain.writeroutine.model.RepeatDay import com.threegap.bitnagil.domain.writeroutine.model.RoutineUpdateType import com.threegap.bitnagil.domain.writeroutine.usecase.EditRoutineUseCase import com.threegap.bitnagil.domain.writeroutine.usecase.RegisterRoutineUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.writeroutine.model.Date import com.threegap.bitnagil.presentation.writeroutine.model.Day import com.threegap.bitnagil.presentation.writeroutine.model.RepeatType @@ -16,7 +15,6 @@ import com.threegap.bitnagil.presentation.writeroutine.model.SelectableDay import com.threegap.bitnagil.presentation.writeroutine.model.SubRoutine import com.threegap.bitnagil.presentation.writeroutine.model.Time import com.threegap.bitnagil.presentation.writeroutine.model.WriteRoutineType -import com.threegap.bitnagil.presentation.writeroutine.model.mvi.WriteRoutineIntent import com.threegap.bitnagil.presentation.writeroutine.model.mvi.WriteRoutineSideEffect import com.threegap.bitnagil.presentation.writeroutine.model.mvi.WriteRoutineState import com.threegap.bitnagil.presentation.writeroutine.model.navarg.WriteRoutineScreenArg @@ -24,8 +22,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container @HiltViewModel(assistedFactory = WriteRoutineViewModel.Factory::class) class WriteRoutineViewModel @AssistedInject constructor( @@ -35,14 +34,16 @@ class WriteRoutineViewModel @AssistedInject constructor( private val getRoutineUseCase: GetRoutineUseCase, private val getRecommendRoutineUseCase: GetRecommendRoutineUseCase, @Assisted private val writeRoutineArg: WriteRoutineScreenArg, -) : MviViewModel( - initState = WriteRoutineState.Init, - savedStateHandle = savedStateHandle, -) { +) : ContainerHost, ViewModel() { @AssistedFactory interface Factory { fun create(writeRoutineArg: WriteRoutineScreenArg): WriteRoutineViewModel } + override val container: Container = container( + savedStateHandle = savedStateHandle, + initialState = WriteRoutineState.Init, + ) + private var routineId: String? = null private var oldSubRoutines: List = listOf() @@ -51,11 +52,13 @@ class WriteRoutineViewModel @AssistedInject constructor( initResource(navigationArg) } - private fun initResource(navigationArg: WriteRoutineScreenArg) { + private fun initResource(navigationArg: WriteRoutineScreenArg) = intent { when (navigationArg) { is WriteRoutineScreenArg.Add -> { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetWriteRoutineType(WriteRoutineType.Add)) + reduce { + state.copy( + writeRoutineType = WriteRoutineType.Add, + ) } navigationArg.baseRoutineId?.let { @@ -64,8 +67,10 @@ class WriteRoutineViewModel @AssistedInject constructor( } } is WriteRoutineScreenArg.Edit -> { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetWriteRoutineType(WriteRoutineType.Edit(updateRoutineFromNowDate = navigationArg.updateRoutineFromNowDate))) + reduce { + state.copy( + writeRoutineType = WriteRoutineType.Edit(updateRoutineFromNowDate = navigationArg.updateRoutineFromNowDate), + ) } navigationArg.routineId.also { @@ -76,168 +81,16 @@ class WriteRoutineViewModel @AssistedInject constructor( } } - private fun loadRoutine(routineId: String) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.GetRoutineLoading) - getRoutineUseCase(routineId).fold( - onSuccess = { routine -> - sendIntent( - WriteRoutineIntent.SetRoutine( - name = routine.name, - repeatDays = routine.repeatDays.map { Day.fromDayOfWeek(it) }, - startTime = Time.fromDomainTimeString(routine.executionTime), - subRoutines = listOf( - routine.subRoutineNames.getOrNull(0) ?: "", - routine.subRoutineNames.getOrNull(1) ?: "", - routine.subRoutineNames.getOrNull(2) ?: "", - ), - startDate = Date.fromString(routine.startDate), - endDate = Date.fromString(routine.endDate), - recommendedRoutineType = null, - ), - ) - }, - onFailure = { - // 실패 케이스 처리 예정 - }, - ) + private fun loadRoutine(routineId: String) = intent { + reduce { + state.copy(loading = true) } - } - - private fun loadRecommendRoutine(recommendRoutineId: String) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.GetRoutineLoading) - getRecommendRoutineUseCase(recommendRoutineId).fold( - onSuccess = { routine -> - oldSubRoutines = routine.recommendSubRoutines.map { SubRoutine.fromDomainRecommendSubRoutine(it) } - sendIntent( - WriteRoutineIntent.SetRoutine( - name = routine.name, - repeatDays = listOf(), - startTime = Time.fromDomainTimeString(routine.executionTime), - subRoutines = listOf( - oldSubRoutines.getOrNull(0)?.name ?: "", - oldSubRoutines.getOrNull(1)?.name ?: "", - oldSubRoutines.getOrNull(2)?.name ?: "", - ), - startDate = Date.now(), - endDate = Date.now(), - recommendedRoutineType = routine.recommendedRoutineType.categoryName, - ), - ) - }, - onFailure = { - // 실패 케이스 처리 예정 - }, - ) - } - } - override suspend fun Syntax.reduceState( - intent: WriteRoutineIntent, - state: WriteRoutineState, - ): WriteRoutineState? { - when (intent) { - WriteRoutineIntent.SelectAllTime -> { - return state.copy( - selectAllTime = !state.selectAllTime, - startTime = Time.AllDay, - ) - } - is WriteRoutineIntent.SelectDay -> { - return state.copy( - repeatDays = state.repeatDays.map { - if (it.day == intent.day) { - it.copy(selected = !it.selected) - } else { - it - } - }, - ) - } - is WriteRoutineIntent.SetRepeatType -> { - return state.copy( - repeatType = intent.repeatType, - ) - } - is WriteRoutineIntent.SetRoutineName -> { - return state.copy( - routineName = intent.name, - ) - } - is WriteRoutineIntent.SetStartTime -> { - return state.copy( - selectAllTime = intent.time == Time.AllDay, - startTime = intent.time, - ) - } - is WriteRoutineIntent.SetSubRoutineName -> { - return state.copy( - subRoutineNames = state.subRoutineNames.mapIndexed { index, subRoutine -> - if (index == intent.index) { - intent.name - } else { - subRoutine - } - }, - ) - } - WriteRoutineIntent.ShowTimePickerBottomSheet -> { - return state.copy( - showTimePickerBottomSheet = true, - ) - } - WriteRoutineIntent.HideTimePickerBottomSheet -> { - return state.copy( - showTimePickerBottomSheet = false, - ) - } - is WriteRoutineIntent.SetWriteRoutineType -> { - return state.copy( - writeRoutineType = intent.writeRoutineType, - ) - } - - WriteRoutineIntent.EditRoutineFailure -> { - return state.copy( - loading = false, - ) - } - WriteRoutineIntent.EditRoutineSuccess -> { - sendSideEffect(WriteRoutineSideEffect.MoveToPreviousScreen) - sendSideEffect(WriteRoutineSideEffect.ShowToast("루틴 수정이 완료되었습니다.")) - - return state.copy( - loading = false, - ) - } - WriteRoutineIntent.EditRoutineLoading -> { - return state.copy( - loading = true, - ) - } - WriteRoutineIntent.RegisterRoutineFailure -> { - return state.copy( - loading = false, - ) - } - WriteRoutineIntent.RegisterRoutineSuccess -> { - sendSideEffect(WriteRoutineSideEffect.MoveToPreviousScreen) - - return null - } - WriteRoutineIntent.RegisterRoutineLoading -> { - return state.copy( - loading = true, - ) - } - - is WriteRoutineIntent.SetRoutine -> { - val selectedDaySet = intent.repeatDays.toSet() + getRoutineUseCase(routineId).fold( + onSuccess = { routine -> + val selectedDaySet = routine.repeatDays.map { Day.fromDayOfWeek(it) } val repeatDays = SelectableDay.defaultList.map { - it.copy( - selected = it.day in selectedDaySet, - ) + it.copy(selected = it.day in selectedDaySet) } val repeatType = if (repeatDays.none { it.selected }) { null @@ -246,281 +99,330 @@ class WriteRoutineViewModel @AssistedInject constructor( } else { RepeatType.DAY } - return state.copy( - routineName = intent.name, - repeatDays = repeatDays, - repeatType = repeatType, - startTime = intent.startTime, - startDate = intent.startDate, - endDate = intent.endDate, - subRoutineNames = intent.subRoutines, - loading = false, - recommendedRoutineType = intent.recommendedRoutineType, - ) - } - - WriteRoutineIntent.GetRoutineLoading -> { - return state.copy( - loading = true, - ) - } - WriteRoutineIntent.ShowStartDatePickerBottomSheet -> { - return state.copy( - showStartDatePickerBottomSheet = true, - ) - } - WriteRoutineIntent.HideEndDatePickerBottomSheet -> { - return state.copy( - showEndDatePickerBottomSheet = false, - ) - } - WriteRoutineIntent.ShowEndDatePickerBottomSheet -> { - return state.copy( - showEndDatePickerBottomSheet = true, - ) - } - WriteRoutineIntent.HideStartDatePickerBottomSheet -> { - return state.copy( - showStartDatePickerBottomSheet = false, - ) - } - is WriteRoutineIntent.SetPeriodUiExpanded -> { - return state.copy( - periodUiExpanded = intent.expanded, - ) - } - is WriteRoutineIntent.SetRepeatDaysUiExpanded -> { - return state.copy( - repeatDaysUiExpanded = intent.expanded, - ) - } - is WriteRoutineIntent.SetStartTimeUiExpanded -> { - return state.copy( - startTimeUiExpanded = intent.expanded, - ) - } - is WriteRoutineIntent.SetSubRoutineUiExpanded -> { - return state.copy( - subRoutineUiExpanded = intent.expanded, - ) - } - is WriteRoutineIntent.SetEndDate -> { - return state.copy( - startDate = Date.min(intent.date, state.startDate), - endDate = intent.date, - ) - } - is WriteRoutineIntent.SetStartDate -> { - return state.copy( - startDate = intent.date, - endDate = Date.max(intent.date, state.endDate), - ) - } - WriteRoutineIntent.SelectNotUseSubRoutines -> { - val toggledSelectNotUseSubRoutines = !state.selectNotUseSubRoutines + reduce { + state.copy( + routineName = routine.name, + repeatDays = repeatDays, + repeatType = repeatType, + startTime = Time.fromDomainTimeString(routine.executionTime), + startDate = Date.fromString(routine.startDate), + endDate = Date.fromString(routine.endDate), + subRoutineNames = listOf( + routine.subRoutineNames.getOrNull(0) ?: "", + routine.subRoutineNames.getOrNull(1) ?: "", + routine.subRoutineNames.getOrNull(2) ?: "", + ), + loading = false, + recommendedRoutineType = null, + ) + } + }, + onFailure = { + // 실패 케이스 처리 예정 + }, + ) + } - return state.copy( - selectNotUseSubRoutines = toggledSelectNotUseSubRoutines, - subRoutineNames = if (toggledSelectNotUseSubRoutines) listOf("", "", "") else state.subRoutineNames, - ) - } + private fun loadRecommendRoutine(recommendRoutineId: String) = intent { + reduce { + state.copy(loading = true) } + getRecommendRoutineUseCase(recommendRoutineId).fold( + onSuccess = { routine -> + oldSubRoutines = routine.recommendSubRoutines.map { SubRoutine.fromDomainRecommendSubRoutine(it) } + + reduce { + state.copy( + routineName = routine.name, + repeatDays = SelectableDay.defaultList, + repeatType = null, + startTime = Time.fromDomainTimeString(routine.executionTime), + startDate = Date.now(), + endDate = Date.now(), + subRoutineNames = listOf( + oldSubRoutines.getOrNull(0)?.name ?: "", + oldSubRoutines.getOrNull(1)?.name ?: "", + oldSubRoutines.getOrNull(2)?.name ?: "", + ), + loading = false, + recommendedRoutineType = routine.recommendedRoutineType.categoryName, + ) + } + }, + onFailure = { + // 실패 케이스 처리 예정 + }, + ) } - fun setRoutineName(name: String) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetRoutineName(name)) + fun setRoutineName(name: String) = intent { + reduce { + state.copy( + routineName = name, + ) } } - fun setSubRoutineName(index: Int, name: String) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetSubRoutineName(index, name)) + fun setSubRoutineName(index: Int, name: String) = intent { + reduce { + state.copy( + subRoutineNames = state.subRoutineNames.mapIndexed { subRoutineIndex, subRoutine -> + if (subRoutineIndex == index) { + name + } else { + subRoutine + } + }, + ) } } - fun selectNotUseSubRoutines() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SelectNotUseSubRoutines) + fun selectNotUseSubRoutines() = intent { + val toggledSelectNotUseSubRoutines = !state.selectNotUseSubRoutines + reduce { + state.copy( + selectNotUseSubRoutines = toggledSelectNotUseSubRoutines, + subRoutineNames = if (toggledSelectNotUseSubRoutines) listOf("", "", "") else state.subRoutineNames, + ) } } - fun selectRepeatType(repeatType: RepeatType) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetRepeatType(repeatType)) + fun selectRepeatType(repeatType: RepeatType) = intent { + reduce { + state.copy( + repeatType = repeatType, + ) } } - fun selectDay(day: Day) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SelectDay(day)) + fun selectDay(day: Day) = intent { + reduce { + state.copy( + repeatDays = state.repeatDays.map { + if (it.day == day) { + it.copy(selected = !it.selected) + } else { + it + } + }, + ) } } - fun selectAllTime() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SelectAllTime) + fun selectAllTime() = intent { + reduce { + state.copy( + selectAllTime = !state.selectAllTime, + startTime = Time.AllDay, + ) } } - fun setStartTime(hour: Int, minute: Int) { + fun setStartTime(hour: Int, minute: Int) = intent { val time = Time(hour = hour, minute = minute) - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetStartTime(time)) + + reduce { + state.copy( + selectAllTime = time == Time.AllDay, + startTime = time, + ) } } - fun showTimePickerBottomSheet() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.ShowTimePickerBottomSheet) + fun showTimePickerBottomSheet() = intent { + reduce { + state.copy( + showTimePickerBottomSheet = true, + ) } } - fun hideTimePickerBottomSheet() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.HideTimePickerBottomSheet) + fun hideTimePickerBottomSheet() = intent { + reduce { + state.copy( + showTimePickerBottomSheet = false, + ) } } - fun showStartDatePickerBottomSheet() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.ShowStartDatePickerBottomSheet) + fun showStartDatePickerBottomSheet() = intent { + reduce { + state.copy( + showStartDatePickerBottomSheet = true, + ) } } - fun hideStartDatePickerBottomSheet() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.HideStartDatePickerBottomSheet) + fun hideStartDatePickerBottomSheet() = intent { + reduce { + state.copy( + showStartDatePickerBottomSheet = false, + ) } } - fun showEndDatePickerBottomSheet() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.ShowEndDatePickerBottomSheet) + fun showEndDatePickerBottomSheet() = intent { + reduce { + state.copy( + showEndDatePickerBottomSheet = true, + ) } } - fun hideEndDatePickerBottomSheet() { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.HideEndDatePickerBottomSheet) + fun hideEndDatePickerBottomSheet() = intent { + reduce { + state.copy( + showEndDatePickerBottomSheet = false, + ) } } - fun setStartDate(date: Date) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetStartDate(date)) + fun setStartDate(date: Date) = intent { + reduce { + state.copy( + startDate = date, + endDate = Date.max(date, state.endDate), + ) } } - fun setEndDate(date: Date) { - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetEndDate(date)) + fun setEndDate(date: Date) = intent { + reduce { + state.copy( + startDate = Date.min(date, state.startDate), + endDate = date, + ) } } - fun toggleSubRoutineUiExpanded() { - val currentSubRoutineUiExpanded = stateFlow.value.subRoutineUiExpanded - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetSubRoutineUiExpanded(!currentSubRoutineUiExpanded)) + fun toggleSubRoutineUiExpanded() = intent { + val currentSubRoutineUiExpanded = state.subRoutineUiExpanded + reduce { + state.copy( + subRoutineUiExpanded = !currentSubRoutineUiExpanded, + ) } } - fun toggleRepeatDaysUiExpanded() { - val currentRepeatDaysUiExpanded = stateFlow.value.repeatDaysUiExpanded - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetRepeatDaysUiExpanded(!currentRepeatDaysUiExpanded)) + fun toggleRepeatDaysUiExpanded() = intent { + val currentRepeatDaysUiExpanded = state.repeatDaysUiExpanded + reduce { + state.copy( + repeatDaysUiExpanded = !currentRepeatDaysUiExpanded, + ) } } - fun togglePeriodUiExpanded() { - val currentPeriodUiExpanded = stateFlow.value.periodUiExpanded - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetPeriodUiExpanded(!currentPeriodUiExpanded)) + fun togglePeriodUiExpanded() = intent { + val currentPeriodUiExpanded = state.periodUiExpanded + reduce { + state.copy( + periodUiExpanded = !currentPeriodUiExpanded, + ) } } - fun toggleStartTimeUiExpanded() { - val currentStartTimeUiExpanded = stateFlow.value.startTimeUiExpanded - viewModelScope.launch { - sendIntent(WriteRoutineIntent.SetStartTimeUiExpanded(!currentStartTimeUiExpanded)) + fun toggleStartTimeUiExpanded() = intent { + val currentStartTimeUiExpanded = state.startTimeUiExpanded + reduce { + state.copy( + startTimeUiExpanded = !currentStartTimeUiExpanded, + ) } } - fun registerRoutine() { - viewModelScope.launch { - val currentState = stateFlow.value + fun registerRoutine() = intent { + val currentState = state - if (currentState.loading) return@launch + if (currentState.loading) return@intent - val startTime = currentState.startTime ?: return@launch + val startTime = currentState.startTime ?: return@intent - val repeatDay = when (currentState.repeatType) { - RepeatType.DAILY -> listOf( - RepeatDay.MON, - RepeatDay.TUE, - RepeatDay.WED, - RepeatDay.THU, - RepeatDay.FRI, - RepeatDay.SAT, - RepeatDay.SUN, - ) + val repeatDay = when (currentState.repeatType) { + RepeatType.DAILY -> listOf( + RepeatDay.MON, + RepeatDay.TUE, + RepeatDay.WED, + RepeatDay.THU, + RepeatDay.FRI, + RepeatDay.SAT, + RepeatDay.SUN, + ) - RepeatType.DAY -> - currentState.repeatDays - .filter { it.selected } - .map { it.day.toRepeatDay() } + RepeatType.DAY -> + currentState.repeatDays + .filter { it.selected } + .map { it.day.toRepeatDay() } - null -> listOf() - } + null -> listOf() + } - when (val writeRoutineType = currentState.writeRoutineType) { - WriteRoutineType.Add -> { - sendIntent(WriteRoutineIntent.RegisterRoutineLoading) - val subRoutines = if (currentState.selectNotUseSubRoutines) emptyList() else currentState.subRoutineNames.filter { it.isNotEmpty() } - val noRepeatRoutine = repeatDay.isEmpty() - - val registerRoutineResult = registerRoutineUseCase( - name = currentState.routineName, - repeatDay = repeatDay, - startTime = startTime.toDomainTime(), - startDate = if (noRepeatRoutine) Date.now().toDomainDate() else currentState.startDate.toDomainDate(), - endDate = if (noRepeatRoutine) Date.now().toDomainDate() else currentState.endDate.toDomainDate(), - subRoutines = subRoutines, - recommendedRoutineType = currentState.recommendedRoutineType, + when (val writeRoutineType = currentState.writeRoutineType) { + WriteRoutineType.Add -> { + reduce { + state.copy( + loading = true, ) + } - if (registerRoutineResult.isSuccess) { - sendIntent(WriteRoutineIntent.RegisterRoutineSuccess) - } else { - sendIntent(WriteRoutineIntent.RegisterRoutineFailure) + val subRoutines = if (currentState.selectNotUseSubRoutines) emptyList() else currentState.subRoutineNames.filter { it.isNotEmpty() } + val noRepeatRoutine = repeatDay.isEmpty() + + val registerRoutineResult = registerRoutineUseCase( + name = currentState.routineName, + repeatDay = repeatDay, + startTime = startTime.toDomainTime(), + startDate = if (noRepeatRoutine) Date.now().toDomainDate() else currentState.startDate.toDomainDate(), + endDate = if (noRepeatRoutine) Date.now().toDomainDate() else currentState.endDate.toDomainDate(), + subRoutines = subRoutines, + recommendedRoutineType = currentState.recommendedRoutineType, + ) + + if (registerRoutineResult.isSuccess) { + postSideEffect(WriteRoutineSideEffect.MoveToPreviousScreen) + } else { + reduce { + state.copy( + loading = false, + ) } } - is WriteRoutineType.Edit -> { - val currentRoutineId = routineId ?: return@launch - val subRoutines = if (currentState.selectNotUseSubRoutines) emptyList() else currentState.subRoutineNames.filter { it.isNotEmpty() } - val routineUpdateType = if (writeRoutineType.updateRoutineFromNowDate) { - RoutineUpdateType.Today - } else { - RoutineUpdateType.Tomorrow - } + } + is WriteRoutineType.Edit -> { + val currentRoutineId = routineId ?: return@intent + val subRoutines = if (currentState.selectNotUseSubRoutines) emptyList() else currentState.subRoutineNames.filter { it.isNotEmpty() } + val routineUpdateType = if (writeRoutineType.updateRoutineFromNowDate) { + RoutineUpdateType.Today + } else { + RoutineUpdateType.Tomorrow + } - sendIntent(WriteRoutineIntent.EditRoutineLoading) - val editRoutineResult = editRoutineUseCase( - routineId = currentRoutineId, - routineUpdateType = routineUpdateType, - name = currentState.routineName, - repeatDay = repeatDay, - startTime = startTime.toDomainTime(), - startDate = currentState.startDate.toDomainDate(), - endDate = currentState.endDate.toDomainDate(), - subRoutines = subRoutines, + reduce { + state.copy( + loading = true, ) + } - if (editRoutineResult.isSuccess) { - sendIntent(WriteRoutineIntent.EditRoutineSuccess) - } else { - sendIntent(WriteRoutineIntent.EditRoutineFailure) + val editRoutineResult = editRoutineUseCase( + routineId = currentRoutineId, + routineUpdateType = routineUpdateType, + name = currentState.routineName, + repeatDay = repeatDay, + startTime = startTime.toDomainTime(), + startDate = currentState.startDate.toDomainDate(), + endDate = currentState.endDate.toDomainDate(), + subRoutines = subRoutines, + ) + + if (editRoutineResult.isSuccess) { + postSideEffect(WriteRoutineSideEffect.MoveToPreviousScreen) + postSideEffect(WriteRoutineSideEffect.ShowToast("루틴 수정이 완료되었습니다.")) + } else { + reduce { + state.copy( + loading = false, + ) } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineIntent.kt deleted file mode 100644 index f3ec406a..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineIntent.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.threegap.bitnagil.presentation.writeroutine.model.mvi - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent -import com.threegap.bitnagil.presentation.writeroutine.model.Date -import com.threegap.bitnagil.presentation.writeroutine.model.Day -import com.threegap.bitnagil.presentation.writeroutine.model.RepeatType -import com.threegap.bitnagil.presentation.writeroutine.model.Time -import com.threegap.bitnagil.presentation.writeroutine.model.WriteRoutineType - -sealed class WriteRoutineIntent : MviIntent { - data class SetRoutineName(val name: String) : WriteRoutineIntent() - data class SetSubRoutineName(val index: Int, val name: String) : WriteRoutineIntent() - data object SelectNotUseSubRoutines : WriteRoutineIntent() - data class SetRepeatType(val repeatType: RepeatType) : WriteRoutineIntent() - data class SelectDay(val day: Day) : WriteRoutineIntent() - data class SetStartTime(val time: Time) : WriteRoutineIntent() - data class SetWriteRoutineType(val writeRoutineType: WriteRoutineType) : WriteRoutineIntent() - data class SetRoutine( - val name: String, - val repeatDays: List, - val startTime: Time, - val subRoutines: List, - val startDate: Date, - val endDate: Date, - val recommendedRoutineType: String?, - ) : WriteRoutineIntent() - data object SelectAllTime : WriteRoutineIntent() - data object ShowTimePickerBottomSheet : WriteRoutineIntent() - data object HideTimePickerBottomSheet : WriteRoutineIntent() - data object ShowStartDatePickerBottomSheet : WriteRoutineIntent() - data object HideStartDatePickerBottomSheet : WriteRoutineIntent() - data object ShowEndDatePickerBottomSheet : WriteRoutineIntent() - data object HideEndDatePickerBottomSheet : WriteRoutineIntent() - data class SetSubRoutineUiExpanded(val expanded: Boolean) : WriteRoutineIntent() - data class SetRepeatDaysUiExpanded(val expanded: Boolean) : WriteRoutineIntent() - data class SetPeriodUiExpanded(val expanded: Boolean) : WriteRoutineIntent() - data class SetStartTimeUiExpanded(val expanded: Boolean) : WriteRoutineIntent() - data class SetStartDate(val date: Date) : WriteRoutineIntent() - data class SetEndDate(val date: Date) : WriteRoutineIntent() - data object RegisterRoutineLoading : WriteRoutineIntent() - data object RegisterRoutineSuccess : WriteRoutineIntent() - data object RegisterRoutineFailure : WriteRoutineIntent() - data object EditRoutineLoading : WriteRoutineIntent() - data object EditRoutineSuccess : WriteRoutineIntent() - data object EditRoutineFailure : WriteRoutineIntent() - data object GetRoutineLoading : WriteRoutineIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineSideEffect.kt index ca5c12a6..c9e52bd6 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.writeroutine.model.mvi -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed class WriteRoutineSideEffect : MviSideEffect { +sealed class WriteRoutineSideEffect { data object MoveToPreviousScreen : WriteRoutineSideEffect() data class ShowToast(val message: String) : WriteRoutineSideEffect() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt index 9078f28b..5f67d6fe 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/mvi/WriteRoutineState.kt @@ -1,6 +1,6 @@ package com.threegap.bitnagil.presentation.writeroutine.model.mvi -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState +import android.os.Parcelable import com.threegap.bitnagil.presentation.writeroutine.model.Date import com.threegap.bitnagil.presentation.writeroutine.model.Day import com.threegap.bitnagil.presentation.writeroutine.model.RepeatType @@ -30,7 +30,7 @@ data class WriteRoutineState( val periodUiExpanded: Boolean, val startTimeUiExpanded: Boolean, val recommendedRoutineType: String?, -) : MviState { +) : Parcelable { companion object { val Init = WriteRoutineState( routineName = "", From c7499c513d62bb37150e501d4798e0481459c531 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 3 Dec 2025 12:40:27 +0900 Subject: [PATCH 05/13] =?UTF-8?q?Refacor:=20Guide=20MviviewModel=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/guide/GuideScreen.kt | 23 ++++---- .../presentation/guide/GuideViewModel.kt | 53 ++++++++----------- .../presentation/guide/model/GuideIntent.kt | 9 ---- .../guide/model/GuideSideEffect.kt | 4 +- .../presentation/guide/model/GuideState.kt | 17 +++--- 5 files changed, 43 insertions(+), 63 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt index cba2e1cf..2e7512c2 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideScreen.kt @@ -1,6 +1,5 @@ package com.threegap.bitnagil.presentation.guide -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -14,24 +13,22 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar -import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.guide.component.atom.GuideButton import com.threegap.bitnagil.presentation.guide.component.template.GuideBottomSheet -import com.threegap.bitnagil.presentation.guide.model.GuideIntent import com.threegap.bitnagil.presentation.guide.model.GuideSideEffect import com.threegap.bitnagil.presentation.guide.model.GuideType +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun GuideScreenContainer( navigateToBack: () -> Unit, viewModel: GuideViewModel = hiltViewModel(), ) { - val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + val uiState by viewModel.collectAsState() - viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is GuideSideEffect.NavigateToBack -> navigateToBack() } @@ -41,14 +38,14 @@ fun GuideScreenContainer( uiState.guideType?.let { guideType -> GuideBottomSheet( guideType = guideType, - onDismissRequest = { viewModel.sendIntent(GuideIntent.OnHideGuideBottomSheet) }, + onDismissRequest = viewModel::onHideGuideBottomSheet, ) } } GuideScreen( - onClickGuideButton = { viewModel.sendIntent(GuideIntent.OnClickGuideButton(it)) }, - onBackClick = { viewModel.sendIntent(GuideIntent.OnBackClick) }, + onClickGuideButton = viewModel::onShowGuideBottomSheet, + onBackClick = viewModel::navigateToBack, ) } @@ -56,12 +53,10 @@ fun GuideScreenContainer( private fun GuideScreen( onClickGuideButton: (GuideType) -> Unit, onBackClick: () -> Unit, - modifier: Modifier = Modifier, ) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() - .background(BitnagilTheme.colors.white) .statusBarsPadding(), ) { BitnagilTopBar( @@ -85,7 +80,7 @@ private fun GuideScreen( } } -@Preview +@Preview(showBackground = true) @Composable private fun GuideScreenPreview() { GuideScreen( diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt index d92b9b73..ca1a64ca 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/GuideViewModel.kt @@ -1,46 +1,39 @@ package com.threegap.bitnagil.presentation.guide -import androidx.lifecycle.SavedStateHandle -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.guide.model.GuideIntent +import androidx.lifecycle.ViewModel import com.threegap.bitnagil.presentation.guide.model.GuideSideEffect import com.threegap.bitnagil.presentation.guide.model.GuideState +import com.threegap.bitnagil.presentation.guide.model.GuideType import dagger.hilt.android.lifecycle.HiltViewModel -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel -class GuideViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, -) : MviViewModel( - initState = GuideState(), - savedStateHandle = savedStateHandle, -) { - override suspend fun Syntax.reduceState( - intent: GuideIntent, - state: GuideState, - ): GuideState? { - val newState = when (intent) { - is GuideIntent.OnClickGuideButton -> { - state.copy( - guideType = intent.guideType, - guideBottomSheetVisible = true, - ) - } +class GuideViewModel @Inject constructor() : ContainerHost, ViewModel() { + + override val container: Container = container(initialState = GuideState.INIT) - is GuideIntent.OnHideGuideBottomSheet -> { - state.copy( - guideType = null, - guideBottomSheetVisible = false, - ) + fun onShowGuideBottomSheet(guideType: GuideType) { + intent { + reduce { + state.copy(guideType = guideType, guideBottomSheetVisible = true) } + } + } - is GuideIntent.OnBackClick -> { - sendSideEffect(GuideSideEffect.NavigateToBack) - null + fun onHideGuideBottomSheet() { + intent { + reduce { + state.copy(guideType = null, guideBottomSheetVisible = false) } } + } - return newState + fun navigateToBack() { + intent { + postSideEffect(GuideSideEffect.NavigateToBack) + } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideIntent.kt deleted file mode 100644 index b28b34ca..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideIntent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.threegap.bitnagil.presentation.guide.model - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent - -sealed class GuideIntent : MviIntent { - data object OnHideGuideBottomSheet : GuideIntent() - data object OnBackClick : GuideIntent() - data class OnClickGuideButton(val guideType: GuideType) : GuideIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideSideEffect.kt index 0cffdc52..8c9e9d3a 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideSideEffect.kt @@ -1,7 +1,5 @@ package com.threegap.bitnagil.presentation.guide.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed interface GuideSideEffect : MviSideEffect { +sealed interface GuideSideEffect { data object NavigateToBack : GuideSideEffect } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt index 4223c533..0375371a 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/guide/model/GuideState.kt @@ -1,10 +1,13 @@ package com.threegap.bitnagil.presentation.guide.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState -import kotlinx.parcelize.Parcelize - -@Parcelize data class GuideState( - val guideType: GuideType? = null, - val guideBottomSheetVisible: Boolean = false, -) : MviState + val guideType: GuideType?, + val guideBottomSheetVisible: Boolean, +) { + companion object { + val INIT = GuideState( + guideType = null, + guideBottomSheetVisible = false, + ) + } +} From 75cd5665c1d8781a950d4cc7bed0f8d1d284611f Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 3 Dec 2025 14:12:50 +0900 Subject: [PATCH 06/13] =?UTF-8?q?Refactor:=20Recommend=20MviViewModel=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecommendRoutineScreen.kt | 38 +++---- .../RecommendRoutineViewModel.kt | 107 +++++++++--------- .../model/RecommendRoutineIntent.kt | 15 --- .../model/RecommendRoutineSideEffect.kt | 7 +- .../model/RecommendRoutineState.kt | 28 +++-- 5 files changed, 97 insertions(+), 98 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt index 84c5a7ac..9ecd7736 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon @@ -38,39 +37,40 @@ import com.threegap.bitnagil.presentation.recommendroutine.component.block.Emoti import com.threegap.bitnagil.presentation.recommendroutine.component.block.RecommendRoutineItem import com.threegap.bitnagil.presentation.recommendroutine.component.template.EmptyRecommendRoutineView import com.threegap.bitnagil.presentation.recommendroutine.component.template.RecommendLevelBottomSheet -import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutineIntent +import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutineSideEffect import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutineState +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun RecommendRoutineScreenContainer( - viewmodel: RecommendRoutineViewModel = hiltViewModel(), + viewModel: RecommendRoutineViewModel = hiltViewModel(), navigateToEmotion: () -> Unit, navigateToRegisterRoutine: (String?) -> Unit, ) { - val uiState by viewmodel.container.stateFlow.collectAsStateWithLifecycle() + val uiState by viewModel.collectAsState() + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is RecommendRoutineSideEffect.NavigateToEmotion -> navigateToEmotion() + is RecommendRoutineSideEffect.NavigateToRegisterRoutine -> navigateToRegisterRoutine(sideEffect.routineId) + } + } if (uiState.recommendLevelBottomSheetVisible) { RecommendLevelBottomSheet( selectedRecommendLevel = uiState.selectedRecommendLevel, - onRecommendLevelSelected = { selectedLevel -> - viewmodel.sendIntent(RecommendRoutineIntent.OnRecommendLevelSelected(selectedLevel)) - }, - onDismiss = { - viewmodel.sendIntent(RecommendRoutineIntent.HideRecommendLevelBottomSheet) - }, + onRecommendLevelSelected = viewModel::updateRecommendLevel, + onDismiss = viewModel::hideRecommendLevelBottomSheet, ) } RecommendRoutineScreen( uiState = uiState, - onCategorySelected = { category -> - viewmodel.sendIntent(RecommendRoutineIntent.OnCategorySelected(category)) - }, - onShowDifficultyBottomSheet = { - viewmodel.sendIntent(RecommendRoutineIntent.ShowRecommendLevelBottomSheet) - }, - onRecommendRoutineByEmotionClick = navigateToEmotion, - onRegisterRoutineClick = navigateToRegisterRoutine, + onCategorySelected = viewModel::updateRoutineCategory, + onShowDifficultyBottomSheet = viewModel::showRecommendLevelBottomSheet, + onRecommendRoutineByEmotionClick = viewModel::navigateToEmotion, + onRegisterRoutineClick = viewModel::navigateToRegisterRoutine, ) } @@ -204,7 +204,7 @@ private fun RecommendRoutineScreen( @Composable private fun RoutineRecommendScreenPreview() { RecommendRoutineScreen( - uiState = RecommendRoutineState(), + uiState = RecommendRoutineState.INIT, onCategorySelected = {}, onShowDifficultyBottomSheet = {}, onRecommendRoutineByEmotionClick = {}, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt index d1fffbd4..00a5b072 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineViewModel.kt @@ -1,13 +1,11 @@ package com.threegap.bitnagil.presentation.recommendroutine -import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUseCase import com.threegap.bitnagil.domain.recommendroutine.model.RecommendCategory import com.threegap.bitnagil.domain.recommendroutine.model.RecommendLevel import com.threegap.bitnagil.domain.recommendroutine.usecase.FetchRecommendRoutinesUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutineIntent import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutineSideEffect import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutineState import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutineUiModel @@ -15,18 +13,19 @@ import com.threegap.bitnagil.presentation.recommendroutine.model.RecommendRoutin import com.threegap.bitnagil.presentation.recommendroutine.model.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class RecommendRoutineViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, private val fetchRecommendRoutinesUseCase: FetchRecommendRoutinesUseCase, private val getEmotionChangeEventFlowUseCase: GetEmotionChangeEventFlowUseCase, -) : MviViewModel( - initState = RecommendRoutineState(), - savedStateHandle = savedStateHandle, -) { +) : ContainerHost, ViewModel() { + + override val container: Container = + container(initialState = RecommendRoutineState.INIT) init { loadRecommendRoutines() @@ -35,49 +34,37 @@ class RecommendRoutineViewModel @Inject constructor( private var recommendRoutines: RecommendRoutinesUiModel = RecommendRoutinesUiModel() - override suspend fun Syntax.reduceState( - intent: RecommendRoutineIntent, - state: RecommendRoutineState, - ): RecommendRoutineState? = when (intent) { - is RecommendRoutineIntent.UpdateLoading -> { - state.copy(isLoading = intent.isLoading) - } - - is RecommendRoutineIntent.LoadRecommendRoutines -> { - state.copy( - isLoading = false, - currentRoutines = getCurrentRoutines(state.selectedCategory, state.selectedRecommendLevel), - emotionMarbleType = recommendRoutines.emotionMarbleType, - ) - } - - is RecommendRoutineIntent.OnCategorySelected -> { - state.copy( - selectedCategory = intent.category, - currentRoutines = getCurrentRoutines(intent.category, state.selectedRecommendLevel), - ) + fun updateRoutineCategory(category: RecommendCategory) { + intent { + reduce { + state.copy( + selectedCategory = category, + currentRoutines = getCurrentRoutines(category, state.selectedRecommendLevel), + ) + } } + } - is RecommendRoutineIntent.ShowRecommendLevelBottomSheet -> { - state.copy(recommendLevelBottomSheetVisible = true) + fun showRecommendLevelBottomSheet() { + intent { + reduce { state.copy(recommendLevelBottomSheetVisible = true) } } + } - is RecommendRoutineIntent.HideRecommendLevelBottomSheet -> { - state.copy(recommendLevelBottomSheetVisible = false) - } - - is RecommendRoutineIntent.OnRecommendLevelSelected -> { - state.copy( - selectedRecommendLevel = intent.recommendLevel, - currentRoutines = getCurrentRoutines(state.selectedCategory, intent.recommendLevel), - ) + fun hideRecommendLevelBottomSheet() { + intent { + reduce { state.copy(recommendLevelBottomSheetVisible = false) } } + } - RecommendRoutineIntent.ClearRecommendLevelFilter -> { - state.copy( - selectedRecommendLevel = null, - currentRoutines = getCurrentRoutines(state.selectedCategory, null), - ) + fun updateRecommendLevel(recommendLevel: RecommendLevel?) { + intent { + reduce { + state.copy( + selectedRecommendLevel = recommendLevel, + currentRoutines = getCurrentRoutines(state.selectedCategory, recommendLevel), + ) + } } } @@ -102,17 +89,35 @@ class RecommendRoutineViewModel @Inject constructor( } private fun loadRecommendRoutines() { - sendIntent(RecommendRoutineIntent.UpdateLoading(true)) - viewModelScope.launch { + intent { + reduce { state.copy(isLoading = true) } fetchRecommendRoutinesUseCase().fold( onSuccess = { - recommendRoutines = it.toUiModel() - sendIntent(RecommendRoutineIntent.LoadRecommendRoutines) + reduce { + recommendRoutines = it.toUiModel() + state.copy( + isLoading = false, + currentRoutines = getCurrentRoutines(state.selectedCategory, state.selectedRecommendLevel), + emotionMarbleType = recommendRoutines.emotionMarbleType, + ) + } }, onFailure = { - sendIntent(RecommendRoutineIntent.UpdateLoading(false)) + reduce { state.copy(isLoading = false) } }, ) } } + + fun navigateToEmotion() { + intent { + postSideEffect(RecommendRoutineSideEffect.NavigateToEmotion) + } + } + + fun navigateToRegisterRoutine(routineId: String) { + intent { + postSideEffect(RecommendRoutineSideEffect.NavigateToRegisterRoutine(routineId)) + } + } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineIntent.kt deleted file mode 100644 index 2ef62943..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineIntent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.threegap.bitnagil.presentation.recommendroutine.model - -import com.threegap.bitnagil.domain.recommendroutine.model.RecommendCategory -import com.threegap.bitnagil.domain.recommendroutine.model.RecommendLevel -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent - -sealed class RecommendRoutineIntent : MviIntent { - data class UpdateLoading(val isLoading: Boolean) : RecommendRoutineIntent() - data object LoadRecommendRoutines : RecommendRoutineIntent() - data class OnCategorySelected(val category: RecommendCategory) : RecommendRoutineIntent() - data class OnRecommendLevelSelected(val recommendLevel: RecommendLevel?) : RecommendRoutineIntent() - data object ShowRecommendLevelBottomSheet : RecommendRoutineIntent() - data object HideRecommendLevelBottomSheet : RecommendRoutineIntent() - data object ClearRecommendLevelFilter : RecommendRoutineIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineSideEffect.kt index 3c5368c1..61c49a4c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineSideEffect.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.presentation.recommendroutine.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -interface RecommendRoutineSideEffect : MviSideEffect +sealed interface RecommendRoutineSideEffect { + data object NavigateToEmotion : RecommendRoutineSideEffect + data class NavigateToRegisterRoutine(val routineId: String) : RecommendRoutineSideEffect +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt index db06f831..6b9e95b9 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutineState.kt @@ -3,18 +3,26 @@ package com.threegap.bitnagil.presentation.recommendroutine.model import com.threegap.bitnagil.domain.recommendroutine.model.EmotionMarbleType import com.threegap.bitnagil.domain.recommendroutine.model.RecommendCategory import com.threegap.bitnagil.domain.recommendroutine.model.RecommendLevel -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState -import kotlinx.parcelize.Parcelize -@Parcelize data class RecommendRoutineState( - val isLoading: Boolean = false, - val currentRoutines: List = emptyList(), - val selectedCategory: RecommendCategory = RecommendCategory.PERSONALIZED, - val recommendLevelBottomSheetVisible: Boolean = false, - val selectedRecommendLevel: RecommendLevel? = null, - val emotionMarbleType: EmotionMarbleType? = null, -) : MviState { + val isLoading: Boolean, + val currentRoutines: List, + val selectedCategory: RecommendCategory, + val recommendLevelBottomSheetVisible: Boolean, + val selectedRecommendLevel: RecommendLevel?, + val emotionMarbleType: EmotionMarbleType?, +) { val shouldShowEmotionButton: Boolean get() = selectedCategory == RecommendCategory.PERSONALIZED && emotionMarbleType == null + + companion object { + val INIT = RecommendRoutineState( + isLoading = false, + currentRoutines = emptyList(), + selectedCategory = RecommendCategory.PERSONALIZED, + recommendLevelBottomSheetVisible = false, + selectedRecommendLevel = null, + emotionMarbleType = null, + ) + } } From 63ac757f372a27409b11b06a8d070476719e06ab Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 3 Dec 2025 19:21:08 +0900 Subject: [PATCH 07/13] =?UTF-8?q?Refactor:=20RoutineList=20MviViewModel=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routinelist/RoutineListScreen.kt | 42 ++-- .../routinelist/RoutineListViewModel.kt | 183 ++++++++---------- .../routinelist/model/RoutineListIntent.kt | 19 -- .../model/RoutineListSideEffect.kt | 4 +- .../routinelist/model/RoutineListState.kt | 28 ++- 5 files changed, 119 insertions(+), 157 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt index 131868be..29408e92 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt @@ -18,32 +18,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar -import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.common.toast.GlobalBitnagilToast import com.threegap.bitnagil.presentation.routinelist.component.template.DeleteConfirmBottomSheet import com.threegap.bitnagil.presentation.routinelist.component.template.EditConfirmBottomSheet import com.threegap.bitnagil.presentation.routinelist.component.template.EmptyRoutineListView import com.threegap.bitnagil.presentation.routinelist.component.template.RoutineDetailsCard import com.threegap.bitnagil.presentation.routinelist.component.template.WeeklyDatePicker -import com.threegap.bitnagil.presentation.routinelist.model.RoutineListIntent import com.threegap.bitnagil.presentation.routinelist.model.RoutineListSideEffect import com.threegap.bitnagil.presentation.routinelist.model.RoutineListState import com.threegap.bitnagil.presentation.routinelist.model.RoutineUiModel +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect import java.time.LocalDate @Composable fun RoutineListScreenContainer( + viewModel: RoutineListViewModel = hiltViewModel(), navigateToBack: () -> Unit, navigateToEditRoutine: (String, Boolean) -> Unit, navigateToAddRoutine: () -> Unit, - viewModel: RoutineListViewModel = hiltViewModel(), ) { - val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + val uiState by viewModel.collectAsState() - viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is RoutineListSideEffect.ShowToast -> GlobalBitnagilToast.showCheck(sideEffect.message) is RoutineListSideEffect.NavigateToBack -> navigateToBack() @@ -58,10 +57,10 @@ fun RoutineListScreenContainer( uiState.selectedRoutine?.let { routine -> DeleteConfirmBottomSheet( isRepeatRoutine = routine.repeatDay.isNotEmpty(), - onDismissRequest = { viewModel.sendIntent(RoutineListIntent.HideDeleteConfirmBottomSheet) }, + onDismissRequest = viewModel::hideDeleteConfirmBottomSheet, onDeleteToday = viewModel::deleteRoutineForToday, onDeleteAll = viewModel::deleteRoutineCompletely, - onCancel = { viewModel.sendIntent(RoutineListIntent.HideDeleteConfirmBottomSheet) }, + onCancel = viewModel::hideDeleteConfirmBottomSheet, ) } } @@ -69,26 +68,20 @@ fun RoutineListScreenContainer( if (uiState.editConfirmBottomSheetVisible) { uiState.selectedRoutine?.let { EditConfirmBottomSheet( - onDismissRequest = { viewModel.sendIntent(RoutineListIntent.HideEditConfirmBottomSheet) }, - onApplyToday = { viewModel.sendIntent(RoutineListIntent.OnApplyTodayClick) }, - onApplyTomorrow = { viewModel.sendIntent(RoutineListIntent.OnApplyTomorrowClick) }, + onDismissRequest = viewModel::hideEditConfirmBottomSheet, + onApplyToday = { viewModel.navigateToEditRoutine(true) }, + onApplyTomorrow = { viewModel.navigateToEditRoutine(false) }, ) } } RoutineListScreen( uiState = uiState, - onDateSelect = { selectedDate -> - viewModel.sendIntent(RoutineListIntent.OnDateSelect(selectedDate)) - }, - onShowDeleteConfirmBottomSheet = { routine -> - viewModel.sendIntent(RoutineListIntent.ShowDeleteConfirmBottomSheet(routine)) - }, - onShowEditConfirmBottomSheet = { routine -> - viewModel.sendIntent(RoutineListIntent.ShowEditConfirmBottomSheet(routine)) - }, - onRegisterRoutineClick = { viewModel.sendIntent(RoutineListIntent.OnRegisterRoutineClick) }, - onBackClick = { viewModel.sendIntent(RoutineListIntent.NavigateToBack) }, + onDateSelect = viewModel::updateDate, + onShowDeleteConfirmBottomSheet = viewModel::showDeleteConfirmBottomSheet, + onShowEditConfirmBottomSheet = viewModel::showEditConfirmBottomSheet, + onRegisterRoutineClick = viewModel::navigateToAddRoutine, + onBackClick = viewModel::navigateToBack, ) } @@ -100,10 +93,9 @@ private fun RoutineListScreen( onShowEditConfirmBottomSheet: (RoutineUiModel) -> Unit, onRegisterRoutineClick: () -> Unit, onBackClick: () -> Unit, - modifier: Modifier = Modifier, ) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() .background(BitnagilTheme.colors.coolGray99) .statusBarsPadding(), @@ -158,7 +150,7 @@ private fun RoutineListScreen( @Composable private fun RoutineListScreenPreview() { RoutineListScreen( - uiState = RoutineListState(), + uiState = RoutineListState.INIT, onDateSelect = {}, onShowDeleteConfirmBottomSheet = {}, onShowEditConfirmBottomSheet = {}, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt index e5113f80..db7e33dc 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt @@ -2,20 +2,22 @@ package com.threegap.bitnagil.presentation.routinelist import android.util.Log import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.threegap.bitnagil.domain.routine.usecase.DeleteRoutineForDayUseCase import com.threegap.bitnagil.domain.routine.usecase.DeleteRoutineUseCase import com.threegap.bitnagil.domain.routine.usecase.FetchWeeklyRoutinesUseCase import com.threegap.bitnagil.domain.writeroutine.usecase.GetWriteRoutineEventFlowUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays -import com.threegap.bitnagil.presentation.routinelist.model.RoutineListIntent import com.threegap.bitnagil.presentation.routinelist.model.RoutineListSideEffect import com.threegap.bitnagil.presentation.routinelist.model.RoutineListState +import com.threegap.bitnagil.presentation.routinelist.model.RoutineUiModel import com.threegap.bitnagil.presentation.routinelist.model.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate import javax.inject.Inject @@ -26,95 +28,51 @@ class RoutineListViewModel @Inject constructor( private val deleteRoutineUseCase: DeleteRoutineUseCase, private val deleteRoutineForDayUseCase: DeleteRoutineForDayUseCase, private val getWriteRoutineEventFlowUseCase: GetWriteRoutineEventFlowUseCase, -) : MviViewModel( - savedStateHandle = savedStateHandle, - initState = RoutineListState( - selectedDate = savedStateHandle.get("selectedDate") - ?.takeIf { it.isNotBlank() } - ?.let { dateString -> - runCatching { LocalDate.parse(dateString) }.getOrNull() - } - ?: LocalDate.now(), - ), -) { +) : ContainerHost, ViewModel() { + + override val container: Container = container(initialState = RoutineListState.INIT) + + private val selectedDate = savedStateHandle.get("selectedDate") + ?.takeIf { it.isNotBlank() } + ?.let { dateString -> + runCatching { LocalDate.parse(dateString) }.getOrNull() + } + ?: LocalDate.now() init { + updateDate(selectedDate) fetchRoutines() observeRoutineChanges() } - override suspend fun Syntax.reduceState( - intent: RoutineListIntent, - state: RoutineListState, - ): RoutineListState? { - val newState = when (intent) { - is RoutineListIntent.UpdateLoading -> state.copy(isLoading = intent.isLoading) - is RoutineListIntent.LoadRoutines -> state.copy(routines = intent.routines) - is RoutineListIntent.OnDateSelect -> state.copy(selectedDate = intent.date) - - is RoutineListIntent.ShowDeleteConfirmBottomSheet -> { - state.copy( - selectedRoutine = intent.routine, - deleteConfirmBottomSheetVisible = true, - ) - } - - is RoutineListIntent.ShowEditConfirmBottomSheet -> { - state.copy( - selectedRoutine = intent.routine, - editConfirmBottomSheetVisible = true, - ) - } - - is RoutineListIntent.HideDeleteConfirmBottomSheet -> state.copy(deleteConfirmBottomSheetVisible = false) - is RoutineListIntent.HideEditConfirmBottomSheet -> state.copy(editConfirmBottomSheetVisible = false) - - is RoutineListIntent.NavigateToBack -> { - sendSideEffect(RoutineListSideEffect.NavigateToBack) - null - } - - is RoutineListIntent.OnRegisterRoutineClick -> { - sendSideEffect(RoutineListSideEffect.NavigateToAddRoutine) - null - } + fun updateDate(selectedDate: LocalDate) { + intent { + reduce { state.copy(selectedDate = selectedDate) } + } + } - is RoutineListIntent.OnApplyTodayClick -> { - val selectedRoutine = state.selectedRoutine - if (selectedRoutine != null) { - sendSideEffect( - RoutineListSideEffect.NavigateToEditRoutine( - routineId = selectedRoutine.routineId, - updateRoutineFromNowDate = true, - ), - ) - } - null - } + fun showDeleteConfirmBottomSheet(routine: RoutineUiModel) { + intent { + reduce { state.copy(selectedRoutine = routine, deleteConfirmBottomSheetVisible = true) } + } + } - is RoutineListIntent.OnApplyTomorrowClick -> { - val selectedRoutine = state.selectedRoutine - if (selectedRoutine != null) { - sendSideEffect( - RoutineListSideEffect.NavigateToEditRoutine( - routineId = selectedRoutine.routineId, - updateRoutineFromNowDate = false, - ), - ) - } - null - } + fun hideDeleteConfirmBottomSheet() { + intent { + reduce { state.copy(deleteConfirmBottomSheetVisible = false) } + } + } - is RoutineListIntent.OnSuccessDeletedRoutine -> { - sendSideEffect(RoutineListSideEffect.ShowToast("삭제가 완료되었습니다.")) - state.copy( - isLoading = false, - deleteConfirmBottomSheetVisible = false, - ) - } + fun showEditConfirmBottomSheet(routine: RoutineUiModel) { + intent { + reduce { state.copy(selectedRoutine = routine, editConfirmBottomSheetVisible = true) } } + } - return newState + fun hideEditConfirmBottomSheet() { + intent { + reduce { state.copy(editConfirmBottomSheetVisible = false) } + } } private fun observeRoutineChanges() { @@ -126,55 +84,80 @@ class RoutineListViewModel @Inject constructor( } private fun fetchRoutines() { - sendIntent(RoutineListIntent.UpdateLoading(true)) - val currentWeek = stateFlow.value.selectedDate.getCurrentWeekDays() - val startDate = currentWeek.first().toString() - val endDate = currentWeek.last().toString() - viewModelScope.launch { + intent { + reduce { state.copy(isLoading = true) } + val currentWeek = state.selectedDate.getCurrentWeekDays() + val startDate = currentWeek.first().toString() + val endDate = currentWeek.last().toString() fetchWeeklyRoutinesUseCase(startDate, endDate).fold( - onSuccess = { routines -> - sendIntent(RoutineListIntent.LoadRoutines(routines.toUiModel())) - sendIntent(RoutineListIntent.UpdateLoading(false)) + onSuccess = { routineSchedule -> + reduce { state.copy(isLoading = false, routines = routineSchedule.toUiModel()) } }, onFailure = { Log.e("RoutineListViewModel", "루틴 가져오기 실패: ${it.message}") - sendIntent(RoutineListIntent.UpdateLoading(false)) + reduce { state.copy(isLoading = false) } }, ) } } fun deleteRoutineCompletely() { - sendIntent(RoutineListIntent.UpdateLoading(true)) - val selectedRoutine = stateFlow.value.selectedRoutine!! - viewModelScope.launch { + intent { + reduce { state.copy(isLoading = true) } + val selectedRoutine = state.selectedRoutine ?: return@intent deleteRoutineUseCase(selectedRoutine.routineId).fold( onSuccess = { fetchRoutines() - sendIntent(RoutineListIntent.OnSuccessDeletedRoutine) + reduce { state.copy(isLoading = false, deleteConfirmBottomSheetVisible = false) } + postSideEffect(RoutineListSideEffect.ShowToast("삭제가 완료되었습니다.")) }, onFailure = { Log.e("RoutineListViewModel", "루틴 삭제 실패: ${it.message}") - sendIntent(RoutineListIntent.UpdateLoading(false)) + reduce { state.copy(isLoading = false) } }, ) } } fun deleteRoutineForToday() { - sendIntent(RoutineListIntent.UpdateLoading(true)) - val selectedRoutine = stateFlow.value.selectedRoutine!! - viewModelScope.launch { + intent { + reduce { state.copy(isLoading = true) } + val selectedRoutine = state.selectedRoutine ?: return@intent deleteRoutineForDayUseCase(selectedRoutine.routineId).fold( onSuccess = { fetchRoutines() - sendIntent(RoutineListIntent.OnSuccessDeletedRoutine) + reduce { state.copy(isLoading = false, deleteConfirmBottomSheetVisible = false) } + postSideEffect(RoutineListSideEffect.ShowToast("삭제가 완료되었습니다.")) }, onFailure = { Log.e("RoutineListViewModel", "루틴 삭제 실패: ${it.message}") - sendIntent(RoutineListIntent.UpdateLoading(false)) + reduce { state.copy(isLoading = false) } }, ) } } + + fun navigateToAddRoutine() { + intent { + postSideEffect(RoutineListSideEffect.NavigateToAddRoutine) + } + } + + fun navigateToEditRoutine(updateFromNow: Boolean) { + intent { + val selectedRoutine = state.selectedRoutine ?: return@intent + postSideEffect( + RoutineListSideEffect.NavigateToEditRoutine( + routineId = selectedRoutine.routineId, + updateRoutineFromNowDate = updateFromNow, + ), + ) + } + } + + fun navigateToBack() { + intent { + postSideEffect(RoutineListSideEffect.NavigateToBack) + } + } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt deleted file mode 100644 index f0f5559c..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.threegap.bitnagil.presentation.routinelist.model - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent -import java.time.LocalDate - -sealed class RoutineListIntent : MviIntent { - data class UpdateLoading(val isLoading: Boolean) : RoutineListIntent() - data class LoadRoutines(val routines: RoutinesUiModel) : RoutineListIntent() - data class OnDateSelect(val date: LocalDate) : RoutineListIntent() - data class ShowDeleteConfirmBottomSheet(val routine: RoutineUiModel) : RoutineListIntent() - data class ShowEditConfirmBottomSheet(val routine: RoutineUiModel) : RoutineListIntent() - data object HideDeleteConfirmBottomSheet : RoutineListIntent() - data object HideEditConfirmBottomSheet : RoutineListIntent() - data object NavigateToBack : RoutineListIntent() - data object OnRegisterRoutineClick : RoutineListIntent() - data object OnApplyTodayClick : RoutineListIntent() - data object OnApplyTomorrowClick : RoutineListIntent() - data object OnSuccessDeletedRoutine : RoutineListIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt index 002762ae..0d4d2e91 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.routinelist.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed interface RoutineListSideEffect : MviSideEffect { +sealed interface RoutineListSideEffect { data object NavigateToBack : RoutineListSideEffect data object NavigateToAddRoutine : RoutineListSideEffect data class NavigateToEditRoutine(val routineId: String, val updateRoutineFromNowDate: Boolean) : RoutineListSideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt index 4b3c891e..aa0b9947 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt @@ -1,22 +1,30 @@ package com.threegap.bitnagil.presentation.routinelist.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays -import kotlinx.parcelize.Parcelize import java.time.LocalDate -@Parcelize data class RoutineListState( - val isLoading: Boolean = false, - val routines: RoutinesUiModel = RoutinesUiModel(), - val selectedRoutine: RoutineUiModel? = null, - val selectedDate: LocalDate = LocalDate.now(), - val deleteConfirmBottomSheetVisible: Boolean = false, - val editConfirmBottomSheetVisible: Boolean = false, -) : MviState { + val isLoading: Boolean, + val routines: RoutinesUiModel, + val selectedRoutine: RoutineUiModel?, + val selectedDate: LocalDate, + val deleteConfirmBottomSheetVisible: Boolean, + val editConfirmBottomSheetVisible: Boolean, +) { val currentWeekDates: List get() = selectedDate.getCurrentWeekDays() val selectedDateRoutines: List get() = routines.routines[selectedDate.toString()]?.routineList ?: emptyList() + + companion object { + val INIT = RoutineListState( + isLoading = false, + routines = RoutinesUiModel(), + selectedRoutine = null, + selectedDate = LocalDate.now(), + deleteConfirmBottomSheetVisible = false, + editConfirmBottomSheetVisible = false, + ) + } } From d3b4c1d69b7ffdf18c0b1c2bec3106a782ba395f Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 3 Dec 2025 19:49:51 +0900 Subject: [PATCH 08/13] =?UTF-8?q?Refactor:=20TermsAgreement=20MviViewModel?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../terms/TermsAgreementScreen.kt | 80 ++++------ .../terms/TermsAgreementViewModel.kt | 140 ++++++++---------- .../terms/model/TermsAgreementIntent.kt | 16 -- .../terms/model/TermsAgreementSideEffect.kt | 4 +- .../terms/model/TermsAgreementState.kt | 23 +-- 5 files changed, 99 insertions(+), 164 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt index 14875dfa..bd6ede7c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementScreen.kt @@ -1,6 +1,5 @@ package com.threegap.bitnagil.presentation.terms -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -15,73 +14,45 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar import com.threegap.bitnagil.presentation.terms.component.TermsAgreementItem import com.threegap.bitnagil.presentation.terms.component.ToggleAllAgreementsItem -import com.threegap.bitnagil.presentation.terms.model.TermsAgreementIntent import com.threegap.bitnagil.presentation.terms.model.TermsAgreementSideEffect import com.threegap.bitnagil.presentation.terms.model.TermsAgreementState +import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun TermsAgreementScreenContainer( + viewModel: TermsAgreementViewModel = hiltViewModel(), navigateToTermsOfService: () -> Unit, navigateToPrivacyPolicy: () -> Unit, navigateToOnBoarding: () -> Unit, navigateToBack: () -> Unit, - viewmodel: TermsAgreementViewModel = hiltViewModel(), ) { - val uiState by viewmodel.stateFlow.collectAsStateWithLifecycle() + val uiState by viewModel.collectAsState() - viewmodel.collectSideEffect { sideEffect -> + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - is TermsAgreementSideEffect.NavigateToPrivacyPolicy -> { - navigateToPrivacyPolicy() - } - - is TermsAgreementSideEffect.NavigateToTermsOfService -> { - navigateToTermsOfService() - } - - is TermsAgreementSideEffect.NavigateToOnBoarding -> { - navigateToOnBoarding() - } - - is TermsAgreementSideEffect.NavigateToBack -> { - navigateToBack() - } + is TermsAgreementSideEffect.NavigateToPrivacyPolicy -> navigateToPrivacyPolicy() + is TermsAgreementSideEffect.NavigateToTermsOfService -> navigateToTermsOfService() + is TermsAgreementSideEffect.NavigateToOnBoarding -> navigateToOnBoarding() + is TermsAgreementSideEffect.NavigateToBack -> navigateToBack() } } TermsAgreementScreen( uiState = uiState, - onToggleAllAgreements = { - viewmodel.sendIntent(TermsAgreementIntent.ToggleAllAgreements(it)) - }, - onToggleTermsOfService = { - viewmodel.sendIntent(TermsAgreementIntent.ToggleTermsOfService(it)) - }, - onTogglePrivacyPolicy = { - viewmodel.sendIntent(TermsAgreementIntent.TogglePrivacyPolicy(it)) - }, - onToggleOverFourteen = { - viewmodel.sendIntent(TermsAgreementIntent.ToggleOverFourteen(it)) - }, - onShowTermsOfService = { - viewmodel.sendIntent(TermsAgreementIntent.ShowTermsOfService) - }, - onShowPrivacyPolicy = { - viewmodel.sendIntent(TermsAgreementIntent.ShowPrivacyPolicy) - }, - onStartButtonClick = { - viewmodel.submitTermsAgreement() - }, - onBackButtonClick = { - viewmodel.sendIntent(TermsAgreementIntent.BackButtonClick) - }, + onToggleAllAgreements = viewModel::updateAllAgreements, + onToggleTermsOfService = viewModel::updateTermsOfService, + onTogglePrivacyPolicy = viewModel::updatePrivacyPolicy, + onToggleOverFourteen = viewModel::updateOverFourteen, + onShowTermsOfService = viewModel::navigateToTermsOfService, + onShowPrivacyPolicy = viewModel::navigateToPrivacyPolicy, + onStartButtonClick = viewModel::submitTermsAgreement, + onBackButtonClick = viewModel::navigateToBack, ) } @@ -89,9 +60,9 @@ fun TermsAgreementScreenContainer( private fun TermsAgreementScreen( uiState: TermsAgreementState, onToggleAllAgreements: (Boolean) -> Unit, - onToggleTermsOfService: (Boolean) -> Unit, - onTogglePrivacyPolicy: (Boolean) -> Unit, - onToggleOverFourteen: (Boolean) -> Unit, + onToggleTermsOfService: () -> Unit, + onTogglePrivacyPolicy: () -> Unit, + onToggleOverFourteen: () -> Unit, onShowTermsOfService: () -> Unit, onShowPrivacyPolicy: () -> Unit, onStartButtonClick: () -> Unit, @@ -101,7 +72,6 @@ private fun TermsAgreementScreen( Column( modifier = modifier .fillMaxSize() - .background(BitnagilTheme.colors.white) .statusBarsPadding(), ) { BitnagilTopBar( @@ -117,7 +87,7 @@ private fun TermsAgreementScreen( ) { Text( text = "빛나길 이용을 위해\n필수 약관에 동의해 주세요.", - color = BitnagilTheme.colors.navy500, + color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.title2Bold, ) @@ -132,7 +102,7 @@ private fun TermsAgreementScreen( TermsAgreementItem( title = "(필수) 서비스 이용약관 동의", - onCheckedChange = { onToggleTermsOfService(!uiState.agreedTermsOfService) }, + onCheckedChange = { onToggleTermsOfService() }, isChecked = uiState.agreedTermsOfService, showMore = true, onClickShowMore = onShowTermsOfService, @@ -140,7 +110,7 @@ private fun TermsAgreementScreen( TermsAgreementItem( title = "(필수) 개인정보 수집·이용 동의", - onCheckedChange = { onTogglePrivacyPolicy(!uiState.agreedPrivacyPolicy) }, + onCheckedChange = { onTogglePrivacyPolicy() }, isChecked = uiState.agreedPrivacyPolicy, showMore = true, onClickShowMore = onShowPrivacyPolicy, @@ -148,7 +118,7 @@ private fun TermsAgreementScreen( TermsAgreementItem( title = "(필수) 만 14세 이상입니다.", - onCheckedChange = { onToggleOverFourteen(!uiState.agreedOverFourteen) }, + onCheckedChange = { onToggleOverFourteen() }, isChecked = uiState.agreedOverFourteen, ) } @@ -167,11 +137,11 @@ private fun TermsAgreementScreen( } } -@Preview +@Preview(showBackground = true) @Composable private fun TermsAgreementScreenPreview() { TermsAgreementScreen( - uiState = TermsAgreementState(), + uiState = TermsAgreementState.INIT, onToggleAllAgreements = {}, onToggleTermsOfService = {}, onTogglePrivacyPolicy = {}, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt index 132ea88a..eb73bd82 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/TermsAgreementViewModel.kt @@ -1,116 +1,94 @@ package com.threegap.bitnagil.presentation.terms import android.util.Log -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.threegap.bitnagil.domain.auth.model.TermsAgreement import com.threegap.bitnagil.domain.auth.usecase.SubmitTermsAgreementUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.terms.model.TermsAgreementIntent import com.threegap.bitnagil.presentation.terms.model.TermsAgreementSideEffect import com.threegap.bitnagil.presentation.terms.model.TermsAgreementState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class TermsAgreementViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, private val submitTermsAgreementUseCase: SubmitTermsAgreementUseCase, -) : MviViewModel( - initState = TermsAgreementState(), - savedStateHandle = savedStateHandle, -) { - override suspend fun Syntax.reduceState( - intent: TermsAgreementIntent, - state: TermsAgreementState, - ): TermsAgreementState? = when (intent) { - is TermsAgreementIntent.SetLoading -> { - state.copy(isLoading = intent.isLoading) - } +) : ContainerHost, ViewModel() { + + override val container: Container = container(initialState = TermsAgreementState.INIT) - is TermsAgreementIntent.ToggleAllAgreements -> { - if (state.isLoading) { - null - } else { + fun updateAllAgreements(agreed: Boolean) { + intent { + if (state.isLoading) return@intent + reduce { state.copy( - agreedTermsOfService = intent.agreed, - agreedPrivacyPolicy = intent.agreed, - agreedOverFourteen = intent.agreed, + agreedTermsOfService = agreed, + agreedPrivacyPolicy = agreed, + agreedOverFourteen = agreed, ) } } + } - is TermsAgreementIntent.ToggleTermsOfService -> { - if (state.isLoading) { - null - } else { - state.copy(agreedTermsOfService = intent.agreed) - } - } - - is TermsAgreementIntent.TogglePrivacyPolicy -> { - if (state.isLoading) { - null - } else { - state.copy(agreedPrivacyPolicy = intent.agreed) - } - } - - is TermsAgreementIntent.ToggleOverFourteen -> { - if (state.isLoading) { - null - } else { - state.copy(agreedOverFourteen = intent.agreed) - } + fun updateTermsOfService() { + intent { + if (state.isLoading) return@intent + reduce { state.copy(agreedTermsOfService = !state.agreedTermsOfService) } } + } - is TermsAgreementIntent.ShowTermsOfService -> { - sendSideEffect(TermsAgreementSideEffect.NavigateToTermsOfService) - null + fun updatePrivacyPolicy() { + intent { + if (state.isLoading) return@intent + reduce { state.copy(agreedPrivacyPolicy = !state.agreedPrivacyPolicy) } } + } - is TermsAgreementIntent.ShowPrivacyPolicy -> { - sendSideEffect(TermsAgreementSideEffect.NavigateToPrivacyPolicy) - null + fun updateOverFourteen() { + intent { + if (state.isLoading) return@intent + reduce { state.copy(agreedOverFourteen = !state.agreedOverFourteen) } } + } - is TermsAgreementIntent.SubmitSuccess -> { - sendSideEffect(TermsAgreementSideEffect.NavigateToOnBoarding) - state.copy(isLoading = false) + fun submitTermsAgreement() { + intent { + reduce { state.copy(isLoading = true) } + val agreement = TermsAgreement( + agreedToTermsOfService = state.agreedTermsOfService, + agreedToPrivacyPolicy = state.agreedPrivacyPolicy, + isOverFourteen = state.agreedOverFourteen, + ) + submitTermsAgreementUseCase(agreement).fold( + onSuccess = { + reduce { state.copy(isLoading = false) } + postSideEffect(TermsAgreementSideEffect.NavigateToOnBoarding) + }, + onFailure = { error -> + Log.e("TermsAgreement", "Submit failed: ${error.message}") + reduce { state.copy(isLoading = false) } + }, + ) } + } - is TermsAgreementIntent.SubmitFailure -> { - state.copy(isLoading = false) + fun navigateToTermsOfService() { + intent { + postSideEffect(TermsAgreementSideEffect.NavigateToTermsOfService) } + } - is TermsAgreementIntent.BackButtonClick -> { - sendSideEffect(TermsAgreementSideEffect.NavigateToBack) - null + fun navigateToPrivacyPolicy() { + intent { + postSideEffect(TermsAgreementSideEffect.NavigateToPrivacyPolicy) } } - fun submitTermsAgreement() { - sendIntent(TermsAgreementIntent.SetLoading(true)) - viewModelScope.launch { - val currentState = container.stateFlow.value - val agreement = TermsAgreement( - agreedToTermsOfService = currentState.agreedTermsOfService, - agreedToPrivacyPolicy = currentState.agreedPrivacyPolicy, - isOverFourteen = currentState.agreedOverFourteen, - ) - - submitTermsAgreementUseCase(agreement) - .fold( - onSuccess = { - sendIntent(TermsAgreementIntent.SubmitSuccess) - }, - onFailure = { error -> - Log.e("TermsAgreement", "Submit failed: ${error.message}") - sendIntent(TermsAgreementIntent.SubmitFailure) - }, - ) + fun navigateToBack() { + intent { + postSideEffect(TermsAgreementSideEffect.NavigateToBack) } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementIntent.kt deleted file mode 100644 index ced68674..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementIntent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.threegap.bitnagil.presentation.terms.model - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent - -sealed class TermsAgreementIntent : MviIntent { - data class SetLoading(val isLoading: Boolean) : TermsAgreementIntent() - data class ToggleAllAgreements(val agreed: Boolean) : TermsAgreementIntent() - data class ToggleTermsOfService(val agreed: Boolean) : TermsAgreementIntent() - data class TogglePrivacyPolicy(val agreed: Boolean) : TermsAgreementIntent() - data class ToggleOverFourteen(val agreed: Boolean) : TermsAgreementIntent() - data object ShowTermsOfService : TermsAgreementIntent() - data object ShowPrivacyPolicy : TermsAgreementIntent() - data object SubmitSuccess : TermsAgreementIntent() - data object SubmitFailure : TermsAgreementIntent() - data object BackButtonClick : TermsAgreementIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementSideEffect.kt index 8a352cf5..9e3e210f 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.terms.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed interface TermsAgreementSideEffect : MviSideEffect { +sealed interface TermsAgreementSideEffect { data object NavigateToTermsOfService : TermsAgreementSideEffect data object NavigateToPrivacyPolicy : TermsAgreementSideEffect data object NavigateToOnBoarding : TermsAgreementSideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementState.kt index 1c78de65..2cc904f6 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/terms/model/TermsAgreementState.kt @@ -1,18 +1,23 @@ package com.threegap.bitnagil.presentation.terms.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState -import kotlinx.parcelize.Parcelize - -@Parcelize data class TermsAgreementState( - val isLoading: Boolean = false, - val agreedTermsOfService: Boolean = false, - val agreedPrivacyPolicy: Boolean = false, - val agreedOverFourteen: Boolean = false, -) : MviState { + val isLoading: Boolean, + val agreedTermsOfService: Boolean, + val agreedPrivacyPolicy: Boolean, + val agreedOverFourteen: Boolean, +) { val isAllAgreed: Boolean get() = agreedTermsOfService && agreedPrivacyPolicy && agreedOverFourteen val submitEnabled: Boolean get() = !isLoading && isAllAgreed + + companion object { + val INIT = TermsAgreementState( + isLoading = false, + agreedTermsOfService = false, + agreedPrivacyPolicy = false, + agreedOverFourteen = false, + ) + } } From c89f031af33e812ce7f87e29e42ba5936976e70e Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 3 Dec 2025 21:17:04 +0900 Subject: [PATCH 09/13] =?UTF-8?q?Refactor:=20Splash=20MviviewModel=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/splash/SplashScreen.kt | 17 +-- .../presentation/splash/SplashViewModel.kt | 100 +++++------------- .../presentation/splash/model/SplashIntent.kt | 13 --- .../splash/model/SplashSideEffect.kt | 4 +- .../presentation/splash/model/SplashState.kt | 22 ++-- 5 files changed, 47 insertions(+), 109 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt index 5801baa4..acfb7764 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashScreen.kt @@ -3,7 +3,6 @@ package com.threegap.bitnagil.presentation.splash import androidx.activity.ComponentActivity import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,28 +20,27 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.presentation.splash.component.template.BitnagilLottieAnimation import com.threegap.bitnagil.presentation.splash.component.template.ForceUpdateDialog import com.threegap.bitnagil.presentation.splash.model.SplashSideEffect import com.threegap.bitnagil.presentation.splash.util.openAppInPlayStore +import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect import kotlin.system.exitProcess @Composable fun SplashScreenContainer( + viewModel: SplashViewModel = hiltViewModel(), navigateToLogin: () -> Unit, navigateToTermsAgreement: () -> Unit, navigateToOnboarding: () -> Unit, navigateToHome: () -> Unit, - viewModel: SplashViewModel = hiltViewModel(), ) { val context = LocalContext.current val activity = context as? ComponentActivity - val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + val uiState by viewModel.collectAsState() viewModel.collectSideEffect { sideEffect -> when (sideEffect) { @@ -71,14 +69,11 @@ fun SplashScreenContainer( @Composable private fun SplashScreen( onCompleted: () -> Unit, - modifier: Modifier = Modifier, ) { var showIcon by remember { mutableStateOf(false) } Column( - modifier = modifier - .fillMaxSize() - .background(BitnagilTheme.colors.white), + modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -88,9 +83,7 @@ private fun SplashScreen( BitnagilLottieAnimation( lottieJson = R.raw.splash_lottie, onComplete = onCompleted, - onStart = { - showIcon = true - }, + onStart = { showIcon = true }, maxFrame = 120, modifier = Modifier .padding(bottom = 36.dp) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt index ac04adc2..d9c037fe 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/SplashViewModel.kt @@ -1,120 +1,74 @@ package com.threegap.bitnagil.presentation.splash -import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.threegap.bitnagil.domain.auth.model.UserRole import com.threegap.bitnagil.domain.auth.usecase.AutoLoginUseCase import com.threegap.bitnagil.domain.version.usecase.CheckUpdateRequirementUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.splash.model.SplashIntent import com.threegap.bitnagil.presentation.splash.model.SplashSideEffect import com.threegap.bitnagil.presentation.splash.model.SplashState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, private val checkUpdateRequirementUseCase: CheckUpdateRequirementUseCase, private val autoLoginUseCase: AutoLoginUseCase, -) : MviViewModel( - initState = SplashState(), - savedStateHandle = savedStateHandle, -) { +) : ContainerHost, ViewModel() { + + override val container: Container = container(initialState = SplashState.INIT) init { performForceUpdateCheck() } - override suspend fun Syntax.reduceState( - intent: SplashIntent, - state: SplashState, - ): SplashState? = - when (intent) { - is SplashIntent.SetUserRole -> { - state.copy( - userRole = intent.userRole, - isAutoLoginCompleted = true, - ) - } - - is SplashIntent.SetForceUpdateResult -> { - state.copy( - forceUpdateRequired = intent.isRequired, - isForceUpdateCheckCompleted = true, - ) - } - - is SplashIntent.NavigateToLogin -> { - sendSideEffect(SplashSideEffect.NavigateToLogin) - null - } - - is SplashIntent.NavigateToHome -> { - sendSideEffect(SplashSideEffect.NavigateToHome) - null - } - - is SplashIntent.NavigateToTermsAgreement -> { - sendSideEffect(SplashSideEffect.NavigateToTermsAgreement) - null - } - - is SplashIntent.NavigateToOnboarding -> { - sendSideEffect(SplashSideEffect.NavigateToOnboarding) - null - } - } - private fun performForceUpdateCheck() { - viewModelScope.launch { + intent { val isUpdateRequired = withTimeoutOrNull(5000) { checkUpdateRequirementUseCase().getOrElse { false } } ?: false + reduce { state.copy(forceUpdateRequired = isUpdateRequired, isForceUpdateCheckCompleted = true) } - sendIntent(SplashIntent.SetForceUpdateResult(isUpdateRequired)) - - if (!isUpdateRequired) { - performAutoLogin() - } + if (!isUpdateRequired) { performAutoLogin() } } } private fun performAutoLogin() { - viewModelScope.launch { + intent { try { val userRole = withTimeoutOrNull(5000) { autoLoginUseCase() } - sendIntent(SplashIntent.SetUserRole(userRole)) + reduce { state.copy(userRole = userRole, isAutoLoginCompleted = true) } } catch (e: Exception) { - sendIntent(SplashIntent.SetUserRole(null)) + reduce { state.copy(userRole = null, isAutoLoginCompleted = true) } } } } fun onAnimationCompleted() { - val splashState = container.stateFlow.value - - if (splashState.forceUpdateRequired) return - - if (!splashState.isAutoLoginCompleted) { - viewModelScope.launch { - delay(100) - onAnimationCompleted() + intent { + if (state.forceUpdateRequired) return@intent + if (!state.isAutoLoginCompleted) { + viewModelScope.launch { + delay(100) + onAnimationCompleted() + } + return@intent } - return - } - when (splashState.userRole) { - UserRole.GUEST -> sendIntent(SplashIntent.NavigateToTermsAgreement) - UserRole.USER -> sendIntent(SplashIntent.NavigateToHome) - UserRole.ONBOARDING -> sendIntent(SplashIntent.NavigateToOnboarding) - else -> sendIntent(SplashIntent.NavigateToLogin) + when (state.userRole) { + UserRole.GUEST -> postSideEffect(SplashSideEffect.NavigateToTermsAgreement) + UserRole.USER -> postSideEffect(SplashSideEffect.NavigateToHome) + UserRole.ONBOARDING -> postSideEffect(SplashSideEffect.NavigateToOnboarding) + else -> postSideEffect(SplashSideEffect.NavigateToLogin) + } } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt deleted file mode 100644 index 1e909bd7..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashIntent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.threegap.bitnagil.presentation.splash.model - -import com.threegap.bitnagil.domain.auth.model.UserRole -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent - -sealed class SplashIntent : MviIntent { - data class SetUserRole(val userRole: UserRole?) : SplashIntent() - data object NavigateToLogin : SplashIntent() - data object NavigateToHome : SplashIntent() - data object NavigateToTermsAgreement : SplashIntent() - data object NavigateToOnboarding : SplashIntent() - data class SetForceUpdateResult(val isRequired: Boolean) : SplashIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt index 455ec559..f55f3d68 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.splash.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed interface SplashSideEffect : MviSideEffect { +sealed interface SplashSideEffect { data object NavigateToLogin : SplashSideEffect data object NavigateToHome : SplashSideEffect data object NavigateToTermsAgreement : SplashSideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt index f4267f6c..6368c7ee 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/splash/model/SplashState.kt @@ -1,13 +1,19 @@ package com.threegap.bitnagil.presentation.splash.model import com.threegap.bitnagil.domain.auth.model.UserRole -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState -import kotlinx.parcelize.Parcelize -@Parcelize data class SplashState( - val userRole: UserRole? = null, - val isAutoLoginCompleted: Boolean = false, - val isForceUpdateCheckCompleted: Boolean = false, - val forceUpdateRequired: Boolean = false, -) : MviState + val userRole: UserRole?, + val isAutoLoginCompleted: Boolean, + val isForceUpdateCheckCompleted: Boolean, + val forceUpdateRequired: Boolean, +) { + companion object { + val INIT = SplashState( + userRole = null, + isAutoLoginCompleted = false, + isForceUpdateCheckCompleted = false, + forceUpdateRequired = false, + ) + } +} From 42bc4af73ae5871eb17b6aa2ee3a2dc8ec9a783f Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 3 Dec 2025 22:03:00 +0900 Subject: [PATCH 10/13] =?UTF-8?q?Refactor:=20Login=20MviViewModel=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/login/LoginScreen.kt | 14 +-- .../presentation/login/LoginViewModel.kt | 91 +++++++------------ .../presentation/login/model/LoginIntent.kt | 10 -- .../login/model/LoginSideEffect.kt | 4 +- .../presentation/login/model/LoginState.kt | 17 ++-- 5 files changed, 46 insertions(+), 90 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt index 2eb22df8..ef41e7ba 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt @@ -37,21 +37,16 @@ import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun LoginScreenContainer( + viewModel: LoginViewModel = hiltViewModel(), navigateToHome: () -> Unit, navigateToTermsAgreement: () -> Unit, - viewModel: LoginViewModel = hiltViewModel(), ) { val context = LocalContext.current viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - is LoginSideEffect.NavigateToHome -> { - navigateToHome() - } - - is LoginSideEffect.NavigateToTermsAgreement -> { - navigateToTermsAgreement() - } + is LoginSideEffect.NavigateToHome -> navigateToHome() + is LoginSideEffect.NavigateToTermsAgreement -> navigateToTermsAgreement() } } @@ -82,7 +77,6 @@ private fun LoginScreen( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier .fillMaxSize() - .background(BitnagilTheme.colors.white) .statusBarsPadding(), ) { Spacer(modifier = Modifier.height(screenHeight * 0.114f)) @@ -136,7 +130,7 @@ private fun LoginScreen( } } -@Preview +@Preview(showBackground = true) @Composable private fun LoginScreenPreview() { BitnagilTheme { diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt index f22831b0..cdd57bb7 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt @@ -1,88 +1,59 @@ package com.threegap.bitnagil.presentation.login import android.util.Log -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.kakao.sdk.auth.model.OAuthToken import com.threegap.bitnagil.domain.auth.usecase.LoginUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.login.model.LoginIntent import com.threegap.bitnagil.presentation.login.model.LoginSideEffect import com.threegap.bitnagil.presentation.login.model.LoginState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, private val loginUseCase: LoginUseCase, -) : MviViewModel( - initState = LoginState(), - savedStateHandle = savedStateHandle, -) { - override suspend fun Syntax.reduceState( - intent: LoginIntent, - state: LoginState, - ): LoginState? = - when (intent) { - is LoginIntent.SetLoading -> { - state.copy(isLoading = intent.isLoading) - } +) : ContainerHost, ViewModel() { - is LoginIntent.LoginSuccess -> { - sendSideEffect( - if (intent.isGuest) { - LoginSideEffect.NavigateToTermsAgreement - } else { - LoginSideEffect.NavigateToHome - }, - ) - state.copy( - isGuest = intent.isGuest, - isLoading = false, - ) - } - - is LoginIntent.KakaoTalkLoginCancel -> { - state.copy(isLoading = false) - } - - is LoginIntent.LoginFailure -> { - state.copy(isLoading = false) - } - } + override val container: Container = container(initialState = LoginState.INIT) fun kakaoLogin(token: OAuthToken?, error: Throwable?) { - viewModelScope.launch { - sendIntent(LoginIntent.SetLoading(true)) + intent { + reduce { state.copy(isLoading = true) } when { - token != null -> { - processKakaoLoginSuccess(token) - } + token != null -> processKakaoLoginSuccess(token) error != null -> { + reduce { state.copy(isLoading = false) } Log.e("KakaoLogin", "카카오 로그인 실패", error) - sendIntent(LoginIntent.LoginFailure) } } } } private suspend fun processKakaoLoginSuccess(token: OAuthToken) { - loginUseCase( - socialAccessToken = token.accessToken, - socialType = "KAKAO", - ).fold( - onSuccess = { - val isGuest = it.role.isGuest() - sendIntent(LoginIntent.LoginSuccess(isGuest = isGuest)) - }, - onFailure = { e -> - sendIntent(LoginIntent.LoginFailure) - Log.e("Login", "${e.message}") - }, - ) + subIntent { + loginUseCase(socialAccessToken = token.accessToken, socialType = KAKAO).fold( + onSuccess = { + val isGuest = it.role.isGuest() + if (isGuest) { + postSideEffect(LoginSideEffect.NavigateToTermsAgreement) + } else { + postSideEffect(LoginSideEffect.NavigateToHome) + } + reduce { state.copy(isLoading = false) } + }, + onFailure = { e -> + reduce { state.copy(isLoading = false) } + Log.e("Login", "${e.message}") + }, + ) + } + } + + companion object { + private const val KAKAO = "KAKAO" } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt deleted file mode 100644 index ade3f445..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.threegap.bitnagil.presentation.login.model - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent - -sealed class LoginIntent : MviIntent { - data class SetLoading(val isLoading: Boolean) : LoginIntent() - data class LoginSuccess(val isGuest: Boolean) : LoginIntent() - data object KakaoTalkLoginCancel : LoginIntent() - data object LoginFailure : LoginIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt index e1caf82a..3876c792 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.login.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed interface LoginSideEffect : MviSideEffect { +sealed interface LoginSideEffect { data object NavigateToHome : LoginSideEffect data object NavigateToTermsAgreement : LoginSideEffect } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt index 1ba7fd01..e84d295b 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt @@ -1,10 +1,13 @@ package com.threegap.bitnagil.presentation.login.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState -import kotlinx.parcelize.Parcelize - -@Parcelize data class LoginState( - val isLoading: Boolean = false, - val isGuest: Boolean = false, -) : MviState + val isLoading: Boolean, + val isGuest: Boolean, +) { + companion object { + val INIT = LoginState( + isLoading = false, + isGuest = false, + ) + } +} From 364c2a879fedd2803aff6bdb0991c31377083ed2 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 3 Dec 2025 22:24:28 +0900 Subject: [PATCH 11/13] =?UTF-8?q?chore:=20=EB=A6=AC=EB=B7=B0=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/presentation/routinelist/RoutineListViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt index db7e33dc..9bdb3193 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt @@ -108,7 +108,7 @@ class RoutineListViewModel @Inject constructor( deleteRoutineUseCase(selectedRoutine.routineId).fold( onSuccess = { fetchRoutines() - reduce { state.copy(isLoading = false, deleteConfirmBottomSheetVisible = false) } + reduce { state.copy(deleteConfirmBottomSheetVisible = false) } postSideEffect(RoutineListSideEffect.ShowToast("삭제가 완료되었습니다.")) }, onFailure = { @@ -126,7 +126,7 @@ class RoutineListViewModel @Inject constructor( deleteRoutineForDayUseCase(selectedRoutine.routineId).fold( onSuccess = { fetchRoutines() - reduce { state.copy(isLoading = false, deleteConfirmBottomSheetVisible = false) } + reduce { state.copy(deleteConfirmBottomSheetVisible = false) } postSideEffect(RoutineListSideEffect.ShowToast("삭제가 완료되었습니다.")) }, onFailure = { From 3efaf0957617375d991ea11f8b53763f934112af Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 4 Dec 2025 19:49:05 +0900 Subject: [PATCH 12/13] =?UTF-8?q?Chore:=20=EC=BD=94=EB=93=9C=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/presentation/routinelist/RoutineListViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt index 9bdb3193..34df31ef 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt @@ -103,8 +103,8 @@ class RoutineListViewModel @Inject constructor( fun deleteRoutineCompletely() { intent { - reduce { state.copy(isLoading = true) } val selectedRoutine = state.selectedRoutine ?: return@intent + reduce { state.copy(isLoading = true) } deleteRoutineUseCase(selectedRoutine.routineId).fold( onSuccess = { fetchRoutines() From ef9a971c1c42c92baaaa71ed5b8947f4a3a82fce Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 6 Dec 2025 20:32:12 +0900 Subject: [PATCH 13/13] =?UTF-8?q?Fix:=20MviViewModel=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20Settin?= =?UTF-8?q?gSideEffect=EA=B0=80=20MviSideEffect=EB=A5=BC=20=EC=83=81?= =?UTF-8?q?=EC=86=8D=ED=95=98=EA=B3=A0=20=EC=9E=88=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/mviviewmodel/MviIntent.kt | 3 - .../common/mviviewmodel/MviSideEffect.kt | 3 - .../common/mviviewmodel/MviState.kt | 5 -- .../common/mviviewmodel/MviViewModel.kt | 31 ------- .../setting/model/mvi/SettingSideEffect.kt | 4 +- .../common/mviviewmodel/MviViewModelTest.kt | 90 ------------------- 6 files changed, 1 insertion(+), 135 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviIntent.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviSideEffect.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviState.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt delete mode 100644 presentation/src/test/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModelTest.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviIntent.kt deleted file mode 100644 index e7ce3f56..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviIntent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.threegap.bitnagil.presentation.common.mviviewmodel - -interface MviIntent diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviSideEffect.kt deleted file mode 100644 index c5eb002e..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviSideEffect.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.threegap.bitnagil.presentation.common.mviviewmodel - -interface MviSideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviState.kt deleted file mode 100644 index da43d869..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.threegap.bitnagil.presentation.common.mviviewmodel - -import android.os.Parcelable - -interface MviState : Parcelable diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt deleted file mode 100644 index f4afb475..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.threegap.bitnagil.presentation.common.mviviewmodel - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import org.orbitmvi.orbit.ContainerHost -import org.orbitmvi.orbit.syntax.Syntax -import org.orbitmvi.orbit.viewmodel.container - -abstract class MviViewModel( - initState: STATE, - savedStateHandle: SavedStateHandle, -) : ContainerHost, ViewModel() { - override val container = container(initialState = initState, savedStateHandle = savedStateHandle) - - val stateFlow: StateFlow get() = container.stateFlow - val sideEffectFlow: Flow get() = container.sideEffectFlow - - protected suspend fun Syntax.sendSideEffect(sideEffect: SIDE_EFFECT) = postSideEffect(sideEffect) - protected abstract suspend fun Syntax.reduceState(intent: INTENT, state: STATE): STATE? - - fun sendIntent(intent: INTENT) = - intent { - val newState = reduceState(intent, state) - - newState?.let { - reduce { newState } - } - } -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt index 2502f31a..e25e6a48 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.setting.model.mvi -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed class SettingSideEffect : MviSideEffect { +sealed class SettingSideEffect { data object NavigateToLogin : SettingSideEffect() data object NavigateToWithdrawal : SettingSideEffect() } diff --git a/presentation/src/test/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModelTest.kt b/presentation/src/test/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModelTest.kt deleted file mode 100644 index d4399c4d..00000000 --- a/presentation/src/test/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModelTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.threegap.bitnagil.presentation.common.mviviewmodel - -import androidx.lifecycle.SavedStateHandle -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import kotlinx.parcelize.Parcelize -import org.junit.Before -import org.junit.Test -import org.orbitmvi.orbit.syntax.Syntax -import org.orbitmvi.orbit.test.test - -@ExperimentalCoroutinesApi -class MviViewModelTest { - private lateinit var sampleMviViewModel: SampleMviViewModel - - @Before - fun setUp() { - sampleMviViewModel = SampleMviViewModel(initState = SampleState(), savedStateHandle = SavedStateHandle()) - } - - @Test - fun `state는 호출 순서대로 갱신되어야 한다`() = - runTest { - sampleMviViewModel.test(testScope = this) { - containerHost.sendIntent(SampleIntent.Increase(number = 1)) - containerHost.sendIntent(SampleIntent.Decrease(number = 2)) - containerHost.sendIntent(SampleIntent.Increase(number = 3)) - - expectState { SampleState(count = 1) } - expectState { SampleState(count = -1) } - expectState { SampleState(count = 2) } - } - } - - @Test - fun `state와 sideEffect는 호출 순서대로 갱신되어야 한다`() = - runTest { - sampleMviViewModel.test(testScope = this) { - containerHost.sendIntent(SampleIntent.Increase(number = 1)) - containerHost.sendIntent(SampleIntent.Clear) - containerHost.sendIntent(SampleIntent.Decrease(number = 2)) - containerHost.sendIntent(SampleIntent.Increase(number = 3)) - - expectState { SampleState(count = 1) } - expectSideEffect(SampleSideEffect.ShowToast("Clear")) - expectState { SampleState() } - expectState { SampleState(count = -2) } - expectState { SampleState(count = 1) } - } - } - - // only for test - private class SampleMviViewModel( - initState: SampleState, - savedStateHandle: SavedStateHandle, - ) : MviViewModel(initState, savedStateHandle) { - override suspend fun Syntax.reduceState( - intent: SampleIntent, - state: SampleState, - ): SampleState? { - val newState = - when (intent) { - is SampleIntent.Decrease -> state.copy(count = state.count - intent.number) - is SampleIntent.Increase -> state.copy(count = state.count + intent.number) - SampleIntent.Clear -> { - sendSideEffect(sideEffect = SampleSideEffect.ShowToast("Clear")) - state.copy(count = 0) - } - } - return newState - } - } - - @Parcelize - private data class SampleState( - val count: Int = 0, - ) : MviState - - private sealed class SampleSideEffect : MviSideEffect { - data class ShowToast(val message: String) : SampleSideEffect() - } - - private sealed class SampleIntent : MviIntent { - data class Increase(val number: Int) : SampleIntent() - - data class Decrease(val number: Int) : SampleIntent() - - object Clear : SampleIntent() - } -}