Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Goal SnackBar 연동 #38

Merged
merged 14 commits into from
Jan 31, 2025
Merged
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.chipichipi.dobedobe.navigation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import com.chipichipi.dobedobe.feature.dashboard.R
import com.chipichipi.dobedobe.feature.dashboard.navigation.DashboardRoute
import com.chipichipi.dobedobe.feature.dashboard.navigation.dashboardScreen
import com.chipichipi.dobedobe.feature.goal.GoalSnackBarType
import com.chipichipi.dobedobe.feature.goal.navigation.goalGraph
import com.chipichipi.dobedobe.feature.goal.navigation.navigateToAddGoal
import com.chipichipi.dobedobe.feature.goal.navigation.navigateToGoalDetail
Expand All @@ -19,6 +27,7 @@ internal fun DobeDobeNavHost(
modifier: Modifier = Modifier,
) {
val navController = appState.navController
val backStackEntry by navController.currentBackStackEntryAsState()

NavHost(
navController = navController,
Expand All @@ -35,11 +44,55 @@ internal fun DobeDobeNavHost(
goalGraph(
onShowSnackbar = onShowSnackbar,
navigateToBack = navController::popBackStack,
sendSnackBarEvent = navController::saveSnackBarEvent,
)

settingScreen(
onShowSnackbar = onShowSnackbar,
navigateToBack = navController::popBackStack,
)
}

GoalSnackBarEffect(backStackEntry, onShowSnackbar)
}

@Composable
private fun GoalSnackBarEffect(
backStackEntry: NavBackStackEntry?,
onShowSnackbar: suspend (String, String?) -> Boolean,
) {
if (backStackEntry == null) return
val snackBarState = backStackEntry.savedStateHandle.getStateFlow(
GoalSnackBarType.KEY,
GoalSnackBarType.IDLE,
)
val addGoalMessage =
stringResource(id = R.string.feature_dashboard_add_goal_snackbar_message)
val editGoalMessage =
stringResource(id = R.string.feature_dashboard_edit_goal_snackbar_message)
val removeGoalMessage =
stringResource(id = R.string.feature_dashboard_remove_goal_snackbar_message)

LaunchedEffect(snackBarState) {
when (snackBarState.value) {
GoalSnackBarType.IDLE -> {}
GoalSnackBarType.ADD -> onShowSnackbar(addGoalMessage, null)
GoalSnackBarType.EDIT -> onShowSnackbar(editGoalMessage, null)
GoalSnackBarType.REMOVE -> onShowSnackbar(removeGoalMessage, null)
}
backStackEntry.removeSnackBarEvent()
}
}

private fun NavController.saveSnackBarEvent(
type: GoalSnackBarType,
) {
val preBackStackEntry = previousBackStackEntry ?: return
if (preBackStackEntry.destination.route == DashboardRoute::class.java.canonicalName) {
preBackStackEntry.savedStateHandle[GoalSnackBarType.KEY] = type
}
}

private fun NavBackStackEntry.removeSnackBarEvent() {
savedStateHandle.remove<GoalSnackBarType>(GoalSnackBarType.KEY)
}
3 changes: 3 additions & 0 deletions feature/dashboard/src/main/res/values-en/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
<string name="feature_dashboard_edit_delete_image">이미지 삭제</string>
<string name="feature_dashboard_bubble_suffix">해낼 거야!</string>
<string name="feature_dashboard_goal_bottom_sheet_title">나의 목표</string>
<string name="feature_dashboard_add_goal_snackbar_message">목표가 추가되었습니다.</string>
<string name="feature_dashboard_remove_goal_snackbar_message">목표가 삭제되었습니다.</string>
<string name="feature_dashboard_edit_goal_snackbar_message">목표가 수정되었습니다.</string>
</resources>
4 changes: 3 additions & 1 deletion feature/dashboard/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@
<string name="feature_dashboard_edit_delete_image">이미지 삭제</string>
<string name="feature_dashboard_bubble_suffix">해낼 거야!</string>
<string name="feature_dashboard_goal_bottom_sheet_title">나의 목표</string>

<string name="feature_dashboard_add_goal_snackbar_message">목표가 추가되었습니다.</string>
<string name="feature_dashboard_remove_goal_snackbar_message">목표가 삭제되었습니다.</string>
<string name="feature_dashboard_edit_goal_snackbar_message">목표가 수정되었습니다.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.koin.androidx.compose.koinViewModel
fun AddGoalRoute(
onShowSnackbar: suspend (String, String?) -> Boolean,
navigateToBack: () -> Unit,
sendSnackBarEvent: (GoalSnackBarType) -> Unit,
viewModel: AddGoalViewModel = koinViewModel(),
) {
val lifecycle = LocalLifecycleOwner.current.lifecycle
Expand All @@ -43,8 +44,11 @@ fun AddGoalRoute(
?.let { stringResource(id = it) }

LaunchedEffect(Unit) {
viewModel.navigateToBackEvent
.onEach { onBack() }
viewModel.addGoalEvent
.onEach {
sendSnackBarEvent(GoalSnackBarType.ADD)
onBack()
}
.flowWithLifecycle(lifecycle)
.launchIn(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class AddGoalViewModel(
initialValue = GoalTitleValidResult.Empty,
)

private val _navigateToBackEvent = Channel<Unit>(capacity = Channel.BUFFERED)
val navigateToBackEvent: Flow<Unit> = _navigateToBackEvent.receiveAsFlow()
private val _addGoalEvent = Channel<Unit>(capacity = Channel.BUFFERED)
val addGoalEvent: Flow<Unit> = _addGoalEvent.receiveAsFlow()

fun changeGoalTitle(title: String) {
goalTitle.value = title
Expand All @@ -39,7 +39,7 @@ class AddGoalViewModel(
viewModelScope.launch {
if (goalValidResult.value.isValid()) {
goalRepository.addGoal(goalTitle.value).onSuccess {
_navigateToBackEvent.send(Unit)
_addGoalEvent.send(Unit)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.chipichipi.dobedobe.feature.goal

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -45,21 +46,33 @@ import org.koin.androidx.compose.koinViewModel
@Composable
internal fun DetailGoalRoute(
onShowSnackbar: suspend (String, String?) -> Boolean,
sendSnackBarEvent: (GoalSnackBarType) -> Unit,
navigateToBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailGoalViewModel = koinViewModel(),
) {
val uiState: DetailGoalUiState by viewModel.uiState.collectAsStateWithLifecycle()
val isGoalChanged: Boolean by viewModel.isGoalChanged.collectAsStateWithLifecycle()
val lifecycle = LocalLifecycleOwner.current.lifecycle
val focusManager = LocalFocusManager.current
val onBack = {
if (isGoalChanged) {
sendSnackBarEvent(GoalSnackBarType.EDIT)
}
focusManager.clearFocus()
navigateToBack()
}

BackHandler {
onBack()
}

LaunchedEffect(Unit) {
viewModel.navigateToBackEvent
.onEach { onBack() }
viewModel.deleteGoalEvent
.onEach {
sendSnackBarEvent(GoalSnackBarType.REMOVE)
onBack()
}
.flowWithLifecycle(lifecycle)
.launchIn(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import com.chipichipi.dobedobe.core.model.Goal
import com.chipichipi.dobedobe.feature.goal.navigation.GoalRoute
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
Expand All @@ -20,17 +23,37 @@ internal class DetailGoalViewModel(
savedStateHandle: SavedStateHandle,
private val goalRepository: GoalRepository,
) : ViewModel() {
private var originalGoal: MutableStateFlow<Goal?> = MutableStateFlow(null)

val uiState: StateFlow<DetailGoalUiState> = savedStateHandle.toRoute<GoalRoute.Detail>()
.let { route -> goalRepository.getGoal(route.id) }
.mapNotNull { it?.let(DetailGoalUiState::Success) }
.onEach {
if (originalGoal.value == null) {
originalGoal.value = it.goal
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = DetailGoalUiState.Loading,
)
val isGoalChanged: StateFlow<Boolean> =
combine(originalGoal, uiState) { original, current ->
if (current is DetailGoalUiState.Success) {
original != current.goal
} else {
false
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)

private val _navigateToBackEvent = Channel<Unit>(capacity = Channel.BUFFERED)
val navigateToBackEvent: Flow<Unit> = _navigateToBackEvent.receiveAsFlow()
private val _deleteGoalEvent = Channel<Unit>(capacity = Channel.BUFFERED)
val deleteGoalEvent: Flow<Unit> = _deleteGoalEvent.receiveAsFlow()

fun changeGoalTitle(id: Long, title: String) {
viewModelScope.launch {
Expand All @@ -56,7 +79,7 @@ internal class DetailGoalViewModel(
viewModelScope.launch {
goalRepository.removeGoal(id)
.onSuccess {
_navigateToBackEvent.send(Unit)
_deleteGoalEvent.send(Unit)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.chipichipi.dobedobe.feature.goal

enum class GoalSnackBarType {
IDLE,
ADD,
EDIT,
REMOVE,
;

companion object {
const val KEY: String = "DashBoardSnackBarType"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.chipichipi.dobedobe.feature.goal.AddGoalRoute
import com.chipichipi.dobedobe.feature.goal.DetailGoalRoute
import com.chipichipi.dobedobe.feature.goal.GoalSnackBarType

fun NavController.navigateToAddGoal(
navOptions: NavOptions? = null,
Expand All @@ -18,19 +19,22 @@ fun NavController.navigateToGoalDetail(

fun NavGraphBuilder.goalGraph(
onShowSnackbar: suspend (String, String?) -> Boolean,
sendSnackBarEvent: (GoalSnackBarType) -> Unit,
navigateToBack: () -> Unit,
) {
composable<GoalRoute.Add> {
AddGoalRoute(
onShowSnackbar = onShowSnackbar,
navigateToBack = navigateToBack,
sendSnackBarEvent = sendSnackBarEvent,
)
}

composable<GoalRoute.Detail> { backStackEntry ->
composable<GoalRoute.Detail> {
DetailGoalRoute(
onShowSnackbar = onShowSnackbar,
navigateToBack = navigateToBack,
sendSnackBarEvent = sendSnackBarEvent,
)
}
}
1 change: 1 addition & 0 deletions feature/goal/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
<string name="feature_goal_title_error_message_too_long">띄어쓰기 포함 20자까지만 쓸 수 있어요</string>

<string name="feature_goal_navigate_back_icon_content_description">뒤로 가기</string>

</resources>