Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[FEAT/#14] 7주차 과제 구현 #15

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions app/src/main/java/org/sopt/and/core/state/UiState.kt

This file was deleted.

61 changes: 61 additions & 0 deletions app/src/main/java/org/sopt/and/core/viewmodel/BaseViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.sopt.and.core.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

abstract class BaseViewModel<State : UiState, SideEffect : UiSideEffect, Event : UiEvent>(
) : ViewModel() {

/**
* [createInitialState] is a function that creates the initial state of the state.
* Its role is to force users to initialize UiState in child viewmodel
*/
private val initialState: State by lazy { createInitialState() }
abstract fun createInitialState(): State

private val _uiState = MutableStateFlow<State>(initialState)
val uiState = _uiState.asStateFlow()
val currentState: State
get() = uiState.value

private val _event: MutableSharedFlow<Event> = MutableSharedFlow<Event>()
val event = _event.asSharedFlow()

private val _sideEffect: MutableSharedFlow<UiSideEffect> = MutableSharedFlow<UiSideEffect>()
val sideEffect = _sideEffect.asSharedFlow()

fun setState(reduce: State.() -> State) {
_uiState.value = currentState.reduce()
}

/**
* [setEvent] is used in UI to send out event which can change UI State
*
* It is set as `open`, making it possible for child viewmodel to override and customize this method
*/
open fun setEvent(event: Event) {
dispatchEvent(event)
}

/**
* [dispatchEvent] is used to wrap event logins with coroutineScope
*
* By doing so, event logics can handle multiple emissions such as `SideEffect`
*/
private fun dispatchEvent(event: Event) = viewModelScope.launch {
handleEvent(event)
}

protected abstract suspend fun handleEvent(event: Event)

fun setSideEffect(sideEffect: SideEffect) {
viewModelScope.launch {
_sideEffect.emit(sideEffect)
}
}
}
3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/core/viewmodel/UiEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.core.viewmodel

interface UiEvent
3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/core/viewmodel/UiSideEffect.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.core.viewmodel

interface UiSideEffect
3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/core/viewmodel/UiState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.core.viewmodel

interface UiState
22 changes: 16 additions & 6 deletions app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
Expand All @@ -18,7 +19,8 @@ import org.sopt.and.presentation.home.component.HomeTopBar
import org.sopt.and.presentation.home.component.HorizontalBannerPager
import org.sopt.and.presentation.home.component.ProgramRow
import org.sopt.and.presentation.home.component.RankedProgramRow
import org.sopt.and.presentation.home.state.HomeUiState
import org.sopt.and.presentation.home.contract.HomeUiEvent
import org.sopt.and.presentation.home.contract.HomeUiState

@Composable
fun HomeRoute(
Expand All @@ -27,10 +29,16 @@ fun HomeRoute(
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

LaunchedEffect(true){
viewModel.initializeHomeState()
}

HomeScreen(
uiState = uiState,
modifier = modifier,
onTabClick = viewModel::updateSelectedTabIndex
onTabClick = { index ->
viewModel.setEvent(HomeUiEvent.OnTabSelected(index))
}
)
}

Expand All @@ -56,10 +64,12 @@ private fun HomeScreen(
}

item {
HorizontalBannerPager(
imageList = uiState.bannerImgList,
modifier = Modifier.wrapContentHeight()
)
if(uiState.bannerImgList.isNotEmpty()) {

Choose a reason for hiding this comment

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

empty 처리 해주는 거 좋네요!!

HorizontalBannerPager(
imageList = uiState.bannerImgList,
modifier = Modifier.wrapContentHeight()
)
}

RankedProgramRow(
title = uiState.rankedSeries?.title.orEmpty(),
Expand Down
39 changes: 19 additions & 20 deletions app/src/main/java/org/sopt/and/presentation/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
package org.sopt.and.presentation.home

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.sopt.and.core.viewmodel.BaseViewModel
import org.sopt.and.domain.repository.RecommendationRepository
import org.sopt.and.presentation.home.state.HomeUiState
import org.sopt.and.presentation.home.contract.HomeUiEvent
import org.sopt.and.presentation.home.contract.HomeUiState
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor(
private val recommendationRepository: RecommendationRepository
) : ViewModel() {
private var _uiState = MutableStateFlow(HomeUiState())
val uiState = _uiState.asStateFlow()
) : BaseViewModel<HomeUiState, Nothing, HomeUiEvent>() {
override fun createInitialState(): HomeUiState = HomeUiState()

init {
initializeHomeState()
override suspend fun handleEvent(event: HomeUiEvent) {
when(event) {
is HomeUiEvent.OnTabSelected -> {
setState { copy(selectedTabIndex = event.index) }
}
}
}

private fun initializeHomeState() = _uiState.update { currentState ->
currentState.copy(
bannerImgList = recommendationRepository.getBannerImages(),
recommendations = recommendationRepository.getRecommendations(),
rankedSeries = recommendationRepository.getMostPopularSeries()
)
}

fun updateSelectedTabIndex(index: Int) = _uiState.update { currentState ->
currentState.copy(selectedTabIndex = index)
fun initializeHomeState() {
setState {
copy(
bannerImgList = recommendationRepository.getBannerImages(),
recommendations = recommendationRepository.getRecommendations(),
rankedSeries = recommendationRepository.getMostPopularSeries()
)
}
Comment on lines +25 to +31
Copy link

@kangyein9892 kangyein9892 Dec 18, 2024

Choose a reason for hiding this comment

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

그 세미나할때 팟장지현이가

✔️ handleEvent
아까 잠깐 설명했는데! 발생하는 Event에 따라 State를 변경시켜주는 친구입니다.
여기서 주의할 점! **setState 함수를 호출하는 것은 여기에서만 이루어져야 합니다.**

라고 했는데 이건 상관이 없을까요...?! 저도 잘 몰라서 물어봅니다...!

Copy link
Contributor

Choose a reason for hiding this comment

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

상태를 변경하는 로직을 한 군데에서만 관리하기 위해서 setState에서만 상태 변경을 해주시는 것이 좋습니다! (자세한 내용은 세미나 자료 참고,,)

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.sopt.and.presentation.home.contract

import org.sopt.and.core.viewmodel.UiEvent

sealed class HomeUiEvent: UiEvent {
data class OnTabSelected(val index: Int): HomeUiEvent()
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.sopt.and.presentation.home.state
package org.sopt.and.presentation.home.contract

import org.sopt.and.core.viewmodel.UiState
import org.sopt.and.domain.entity.HomeRecommendation

data class HomeUiState(
val selectedTabIndex: Int = 0,
val bannerImgList: List<Int> = emptyList(),
val recommendations: List<HomeRecommendation> = emptyList(),
val rankedSeries: HomeRecommendation? = null
)
) : UiState
41 changes: 26 additions & 15 deletions app/src/main/java/org/sopt/and/presentation/mypage/MyPageScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ import org.sopt.and.core.designsystem.theme.Grey200
import org.sopt.and.core.designsystem.theme.WavveBackground
import org.sopt.and.core.designsystem.theme.White
import org.sopt.and.core.extension.noRippleClickable
import org.sopt.and.domain.entity.Program
import org.sopt.and.core.preference.PreferenceUtil.Companion.LocalPreference
import org.sopt.and.domain.entity.Program
import org.sopt.and.presentation.mypage.component.ProfileLogGroup
import org.sopt.and.presentation.mypage.component.ProfilePurchaseGroup
import org.sopt.and.presentation.mypage.component.ProfileTopBar
import org.sopt.and.presentation.mypage.state.MyPageUiState
import org.sopt.and.presentation.mypage.contract.MyPageSideEffect
import org.sopt.and.presentation.mypage.contract.MyPageUiEvent

@Composable
fun MyPageRoute(
Expand All @@ -56,6 +57,7 @@ fun MyPageRoute(
viewModel: MyPageViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val starredProgram by viewModel.starredState.collectAsStateWithLifecycle()

val snackBarHost = remember { SnackbarHostState() }
val lifecycleOwner = LocalLifecycleOwner.current
Expand Down Expand Up @@ -83,22 +85,23 @@ fun MyPageRoute(
MyPageScreen(
hobby = uiState.hobby,
snackBarHost = snackBarHost,
onLogoutButtonClick = viewModel::onLogoutButtonClick,
onLogoutButtonClick = {
viewModel.setEvent(MyPageUiEvent.OnLogoutButtonClick)
},
onProgramPress = { program ->
with(viewModel) {
updatePressedProgram(program)
updateDeleteDialogVisibility(visibility = true)
}
viewModel.setEvent(MyPageUiEvent.OnStarredProgramPressed(program))
},
uiState = uiState
starredPrograms = starredProgram
)


FloatingActionButton(
onClick = { viewModel.updateSearchDialogVisibility(true) },
shape = CircleShape,
containerColor = Color.Blue,
contentColor = White,
onClick = {
viewModel.setEvent(MyPageUiEvent.OnFAButtonClick)
},
modifier = Modifier
.wrapContentSize()
.align(Alignment.BottomEnd)
Expand All @@ -115,8 +118,12 @@ fun MyPageRoute(

if (uiState.searchDialogVisibility) {
SearchDialog(
onDismissRequest = { viewModel.updateSearchDialogVisibility(false) },
onItemSelect = viewModel::onInsertProgram,
onDismissRequest = {
viewModel.setEvent(MyPageUiEvent.OnSearchDialogDismissed)
},
onItemSelect = { program ->
viewModel.setEvent(MyPageUiEvent.OnSearchProgramSelected(program))
},
)
}
if (uiState.deleteDialogVisibility) {
Expand All @@ -126,8 +133,12 @@ fun MyPageRoute(
R.string.dialog_delete_content,
uiState.pressedProgram?.title.orEmpty()
),
onDismissRequest = { viewModel.updateDeleteDialogVisibility(visibility = false) },
onConfirm = viewModel::onConfirmDelete
onDismissRequest = {
viewModel.setEvent(MyPageUiEvent.OnDeleteDialogDismissed)
},
onConfirm = {
viewModel.setEvent(MyPageUiEvent.OnDeleteProgramConfirmed)
}
)
}

Expand All @@ -136,7 +147,7 @@ fun MyPageRoute(
@Composable
private fun MyPageScreen(
hobby: String,
uiState: MyPageUiState,
starredPrograms: List<Program>,
snackBarHost: SnackbarHostState,
onLogoutButtonClick: () -> Unit,
onProgramPress: (Program) -> Unit,
Expand Down Expand Up @@ -179,7 +190,7 @@ private fun MyPageScreen(
title = stringResource(R.string.mypage_content_title2),
subTitle = stringResource(R.string.mypage_content_empty2),
onItemPress = onProgramPress,
list = uiState.starredProgram,
list = starredPrograms,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp)
Expand Down

This file was deleted.

Loading