Skip to content

Commit

Permalink
[AN/USER] refactor: LiveData to Flow 리팩터링 (축제 목록, 마이페이지) (#493)
Browse files Browse the repository at this point in the history
* refactor: 축제 목록 화면 Flow 적용

* refactor: 마이페이지 화면 Flow 적용

* test: 마이페이지 테스트 중복 코드 함수 분리

* fix: repeatOnStarted 변경사항 반영

* refactor: asStateFlow, asSharedFlow 사용

* test: cancelAndIgnoreRemainingEvents로 종료 명시

* test: cancelAndIgnoreRemainingEvents 제거
  • Loading branch information
EmilyCh0 authored and BGuga committed Oct 17, 2023
1 parent 530887b commit 55aea7e
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.festago.festago.R
import com.festago.festago.databinding.FragmentFestivalListBinding
import com.festago.festago.presentation.ui.home.ticketlist.TicketListFragment
import com.festago.festago.presentation.ui.ticketreserve.TicketReserveActivity
import com.festago.festago.presentation.util.repeatOnStarted
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
Expand Down Expand Up @@ -38,12 +39,16 @@ class FestivalListFragment : Fragment(R.layout.fragment_festival_list) {
}

private fun initObserve() {
vm.uiState.observe(viewLifecycleOwner) {
binding.uiState = it
updateUi(it)
repeatOnStarted(viewLifecycleOwner) {
vm.uiState.collect {
binding.uiState = it
updateUi(it)
}
}
vm.event.observe(viewLifecycleOwner) {
handleEvent(it)
repeatOnStarted(viewLifecycleOwner) {
vm.event.collect {
handleEvent(it)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.festago.festago.presentation.ui.home.festivallist

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.analytics.logNetworkFailure
import com.festago.festago.presentation.ui.home.festivallist.FestivalListEvent.ShowTicketReserve
import com.festago.festago.presentation.util.MutableSingleLiveData
import com.festago.festago.presentation.util.SingleLiveData
import com.festago.festago.repository.FestivalRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -19,11 +21,12 @@ class FestivalListViewModel @Inject constructor(
private val festivalRepository: FestivalRepository,
private val analyticsHelper: AnalyticsHelper,
) : ViewModel() {
private val _uiState = MutableLiveData<FestivalListUiState>(FestivalListUiState.Loading)
val uiState: LiveData<FestivalListUiState> = _uiState

private val _event = MutableSingleLiveData<FestivalListEvent>()
val event: SingleLiveData<FestivalListEvent> = _event
private val _uiState = MutableStateFlow<FestivalListUiState>(FestivalListUiState.Loading)
val uiState: StateFlow<FestivalListUiState> = _uiState.asStateFlow()

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

fun loadFestivals() {
viewModelScope.launch {
Expand All @@ -49,7 +52,9 @@ class FestivalListViewModel @Inject constructor(
}

fun showTicketReserve(festivalId: Long) {
_event.setValue(ShowTicketReserve(festivalId))
viewModelScope.launch {
_event.emit(ShowTicketReserve(festivalId))
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.festago.festago.presentation.ui.home.HomeActivity
import com.festago.festago.presentation.ui.selectschool.SelectSchoolActivity
import com.festago.festago.presentation.ui.signin.SignInActivity
import com.festago.festago.presentation.ui.tickethistory.TicketHistoryActivity
import com.festago.festago.presentation.util.repeatOnStarted
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
Expand Down Expand Up @@ -40,26 +41,38 @@ class MyPageFragment : Fragment(R.layout.fragment_my_page) {
}

private fun initObserve() {
vm.uiState.observe(viewLifecycleOwner) { uiState ->
binding.uiState = uiState
when (uiState) {
is MyPageUiState.Loading, is MyPageUiState.Error -> Unit

is MyPageUiState.Success -> handleSuccess(uiState)
repeatOnStarted(viewLifecycleOwner) {
vm.uiState.collect { uiState ->
handleUiState(uiState)
}
binding.srlMyPage.isRefreshing = false
}
vm.event.observe(viewLifecycleOwner) { event ->
when (event) {
is MyPageEvent.ShowSignIn -> handleShowSignInEvent()
is MyPageEvent.SignOutSuccess -> handleSignOutSuccessEvent()
is MyPageEvent.DeleteAccountSuccess -> handleDeleteAccountSuccess()
is MyPageEvent.ShowTicketHistory -> handleShowTicketHistory()
is MyPageEvent.ShowConfirmDelete -> handleShowConfirmDelete()
repeatOnStarted(viewLifecycleOwner) {
vm.event.collect { event ->
handleEvent(event)
}
}
}

private fun handleUiState(uiState: MyPageUiState) {
binding.uiState = uiState
when (uiState) {
is MyPageUiState.Loading, is MyPageUiState.Error -> Unit

is MyPageUiState.Success -> handleSuccess(uiState)
}
binding.srlMyPage.isRefreshing = false
}

private fun handleEvent(event: MyPageEvent) {
when (event) {
is MyPageEvent.ShowSignIn -> handleShowSignInEvent()
is MyPageEvent.SignOutSuccess -> handleSignOutSuccessEvent()
is MyPageEvent.DeleteAccountSuccess -> handleDeleteAccountSuccess()
is MyPageEvent.ShowTicketHistory -> handleShowTicketHistory()
is MyPageEvent.ShowConfirmDelete -> handleShowConfirmDelete()
}
}

private fun handleShowSignInEvent() {
startActivity(SignInActivity.getIntent(requireContext()))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.festago.festago.presentation.ui.home.mypage

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.analytics.logNetworkFailure
import com.festago.festago.presentation.util.MutableSingleLiveData
import com.festago.festago.presentation.util.SingleLiveData
import com.festago.festago.repository.AuthRepository
import com.festago.festago.repository.TicketRepository
import com.festago.festago.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -24,16 +26,18 @@ class MyPageViewModel @Inject constructor(
private val analyticsHelper: AnalyticsHelper,
) : ViewModel() {

private val _uiState = MutableLiveData<MyPageUiState>(MyPageUiState.Loading)
val uiState: LiveData<MyPageUiState> = _uiState
private val _uiState = MutableStateFlow<MyPageUiState>(MyPageUiState.Loading)
val uiState: StateFlow<MyPageUiState> = _uiState.asStateFlow()

private val _event = MutableSingleLiveData<MyPageEvent>()
val event: SingleLiveData<MyPageEvent> = _event
private val _event = MutableSharedFlow<MyPageEvent>()
val event: SharedFlow<MyPageEvent> = _event.asSharedFlow()

fun loadUserInfo() {
if (!authRepository.isSigned) {
_event.setValue(MyPageEvent.ShowSignIn)
_uiState.value = MyPageUiState.Error
viewModelScope.launch {
_event.emit(MyPageEvent.ShowSignIn)
_uiState.value = MyPageUiState.Error
}
return
}
viewModelScope.launch {
Expand All @@ -59,7 +63,7 @@ class MyPageViewModel @Inject constructor(
viewModelScope.launch {
authRepository.signOut()
.onSuccess {
_event.setValue(MyPageEvent.SignOutSuccess)
_event.emit(MyPageEvent.SignOutSuccess)
_uiState.value = MyPageUiState.Error
}.onFailure {
_uiState.value = MyPageUiState.Error
Expand All @@ -72,14 +76,16 @@ class MyPageViewModel @Inject constructor(
}

fun showConfirmDelete() {
_event.setValue(MyPageEvent.ShowConfirmDelete)
viewModelScope.launch {
_event.emit(MyPageEvent.ShowConfirmDelete)
}
}

fun deleteAccount() {
viewModelScope.launch {
authRepository.deleteAccount()
.onSuccess {
_event.setValue(MyPageEvent.DeleteAccountSuccess)
_event.emit(MyPageEvent.DeleteAccountSuccess)
_uiState.value = MyPageUiState.Error
}.onFailure {
_uiState.value = MyPageUiState.Error
Expand All @@ -92,7 +98,9 @@ class MyPageViewModel @Inject constructor(
}

fun showTicketHistory() {
_event.setValue(MyPageEvent.ShowTicketHistory)
viewModelScope.launch {
_event.emit(MyPageEvent.ShowTicketHistory)
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.festago.festago.presentation.ui.home.festivallist

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.model.Festival
import com.festago.festago.repository.FestivalRepository
Expand All @@ -11,16 +11,17 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.time.LocalDate

class FestivalListViewModelTest {

private lateinit var vm: FestivalListViewModel
private lateinit var festivalRepository: FestivalRepository
private lateinit var analyticsHelper: AnalyticsHelper
Expand All @@ -35,9 +36,6 @@ class FestivalListViewModelTest {
)
}

@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()

@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
Expand All @@ -53,14 +51,18 @@ class FestivalListViewModelTest {
Dispatchers.resetMain()
}

@Test
fun `축제 목록 받아오기에 성공하면 성공 상태이고 축제 목록을 반환한다`() {
// given
private fun `축제 목록 요청 결과가 다음과 같을 때`(result: Result<List<Festival>>) {
coEvery {
festivalRepository.loadFestivals()
} answers {
Result.success(fakeFestivals)
result
}
}

@Test
fun `축제 목록 받아오기에 성공하면 성공 상태이고 축제 목록을 반환한다`() {
// given
`축제 목록 요청 결과가 다음과 같을 때`(Result.success(fakeFestivals))

// when
vm.loadFestivals()
Expand All @@ -70,9 +72,9 @@ class FestivalListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(FestivalListUiState.Success::class.java)

// and
assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(true)
assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(true)
assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)

// and
val actual = (vm.uiState.value as FestivalListUiState.Success).festivals
Expand All @@ -85,11 +87,7 @@ class FestivalListViewModelTest {
@Test
fun `축제 목록 받아오기에 실패하면 에러 상태다`() {
// given
coEvery {
festivalRepository.loadFestivals()
} answers {
Result.failure(Exception())
}
`축제 목록 요청 결과가 다음과 같을 때`(Result.failure(Exception()))

// when
vm.loadFestivals()
Expand All @@ -99,9 +97,9 @@ class FestivalListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(FestivalListUiState.Error::class.java)

// and
assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false)
assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true)
assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false)
assertThat(vm.uiState.value.shouldShowError).isEqualTo(true)
}
softly.assertAll()
}
Expand All @@ -124,21 +122,24 @@ class FestivalListViewModelTest {
assertThat(vm.uiState.value).isInstanceOf(FestivalListUiState.Loading::class.java)

// and
assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false)
assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(true)
assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false)
assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false)
assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true)
assertThat(vm.uiState.value.shouldShowError).isEqualTo(false)
}
softly.assertAll()
}

@Test
fun `티켓 예매를 열면 티켓 예매 열기 이벤트가 발생한다`() {
// when
val fakeFestivalId = 1L
vm.showTicketReserve(fakeFestivalId)
fun `티켓 예매를 열면 티켓 예매 열기 이벤트가 발생한다`() = runTest {

// then
assertThat(vm.event.getValue()).isInstanceOf(FestivalListEvent.ShowTicketReserve::class.java)
vm.event.test {
// when
val fakeFestivalId = 1L
vm.showTicketReserve(fakeFestivalId)

// then
assertThat(awaitItem()).isExactlyInstanceOf(FestivalListEvent.ShowTicketReserve::class.java)
}
}

private fun Festival.toUiState() = FestivalItemUiState(
Expand Down
Loading

0 comments on commit 55aea7e

Please sign in to comment.