Skip to content

Commit

Permalink
[AN/USER] refactor: LiveData to Flow 리팩터링 (티켓 제시, 로그인 화면) (#494) (#498)
Browse files Browse the repository at this point in the history
* refactor: LiveData에서 UiState로 변경전에 Legacy로 변경

* refactor: LiveData에서 StateFlow로 변경

* refactor: 리포지터리 플로우로  변경

* refactor: combine으로 데이터를 입력을 받도록 변경

* refactor: Flow 래핑한 Repository를 다시 래핑해제

* refactor: loadTicket에서 TicketCode까지 받아오는 기능 제거

* refaactor: 티켓 입장 및 테스트 케이스 변경

* refactor: 로그인 화면 플로우로 변경

* refactor: LiveData를 위한 규칙 제거

* refactor: 중복되는 viewModelScope 제거

* refactor: 내부 함수 private 적용

* refactor: 파타미터 네이밍을 명시적으로 변경

* refactor: 리베이스 후 생긴 버그 수정

* test: 테스트 given절 네이밍 수정

* test: given절 용도에 맞는 함수로 변경

* test: 로그인 화면 테스트코드 수정
  • Loading branch information
re4rk authored Oct 7, 2023
1 parent 1285924 commit 31cfe5e
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.festago.festago.databinding.ActivitySignInBinding
import com.festago.festago.presentation.ui.customview.OkDialogFragment
import com.festago.festago.presentation.ui.home.HomeActivity
import com.festago.festago.presentation.util.loginWithKakao
import com.festago.festago.presentation.util.repeatOnStarted
import com.kakao.sdk.user.UserApiClient
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -44,15 +45,19 @@ class SignInActivity : AppCompatActivity() {
}

private fun initObserve() {
vm.event.observe(this) { event ->
when (event) {
SignInEvent.ShowSignInPage -> handleSignInEvent()
SignInEvent.SignInSuccess -> handleSuccessEvent()
SignInEvent.SignInFailure -> handleFailureEvent()
repeatOnStarted(this) {
vm.event.collect {
handleEvent(it)
}
}
}

private fun handleEvent(event: SignInEvent) = when (event) {
SignInEvent.ShowSignInPage -> handleSignInEvent()
SignInEvent.SignInSuccess -> handleSuccessEvent()
SignInEvent.SignInFailure -> handleFailureEvent()
}

private fun initComment() {
val spannableStringBuilder = SpannableStringBuilder(
getString(R.string.mypage_tv_signin_description),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ 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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -18,26 +17,22 @@ class SignInViewModel @Inject constructor(
private val analyticsHelper: AnalyticsHelper,
) : ViewModel() {

private val _event = MutableSingleLiveData<SignInEvent>()
val event: SingleLiveData<SignInEvent> = _event

private val exceptionHandler: CoroutineExceptionHandler =
CoroutineExceptionHandler { _, throwable ->
_event.setValue(SignInEvent.SignInFailure)
analyticsHelper.logNetworkFailure(KEY_SIGN_IN_LOG, throwable.message.toString())
}
private val _event = MutableSharedFlow<SignInEvent>()
val event: SharedFlow<SignInEvent> = _event

fun signInKakao() {
_event.setValue(SignInEvent.ShowSignInPage)
viewModelScope.launch {
_event.emit(SignInEvent.ShowSignInPage)
}
}

fun signIn(token: String) {
viewModelScope.launch(exceptionHandler) {
viewModelScope.launch {
authRepository.signIn(SOCIAL_TYPE_KAKAO, token)
.onSuccess {
_event.setValue(SignInEvent.SignInSuccess)
_event.emit(SignInEvent.SignInSuccess)
}.onFailure {
_event.setValue(SignInEvent.SignInFailure)
_event.emit(SignInEvent.SignInFailure)
analyticsHelper.logNetworkFailure(KEY_SIGN_IN_LOG, it.message.toString())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.festago.festago.presentation.util.repeatOnStarted
import com.google.zxing.BarcodeFormat
import com.journeyapps.barcodescanner.BarcodeEncoder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest

@AndroidEntryPoint
class TicketEntryActivity : AppCompatActivity() {
Expand Down Expand Up @@ -52,12 +53,14 @@ class TicketEntryActivity : AppCompatActivity() {
}

private fun initObserve() {
vm.uiState.observe(this) { uiState ->
binding.uiState = uiState
when (uiState) {
is TicketEntryUiState.Loading, is TicketEntryUiState.Error -> Unit
is TicketEntryUiState.Success -> {
handleSuccess(uiState)
repeatOnStarted(this) {
vm.uiState.collectLatest { uiState ->
binding.uiState = uiState
when (uiState) {
is TicketEntryUiState.Loading, is TicketEntryUiState.Error -> Unit
is TicketEntryUiState.Success -> {
handleSuccess(uiState)
}
}
}
}
Expand All @@ -75,6 +78,7 @@ class TicketEntryActivity : AppCompatActivity() {

private fun initView(currentTicketId: Long) {
vm.loadTicket(currentTicketId)
vm.loadTicketCode(currentTicketId)
}

private fun handleSuccess(uiState: TicketEntryUiState.Success) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.festago.festago.presentation.ui.ticketentry

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.model.Ticket
import com.festago.festago.model.TicketCode
import com.festago.festago.model.timer.Timer
import com.festago.festago.model.timer.TimerListener
import com.festago.festago.repository.TicketRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -20,84 +25,81 @@ class TicketEntryViewModel @Inject constructor(
private val analyticsHelper: AnalyticsHelper,
) : ViewModel() {

private val _uiState = MutableLiveData<TicketEntryUiState>()
val uiState: LiveData<TicketEntryUiState> = _uiState
private val ticketFlow = MutableSharedFlow<Result<Ticket>>()

private val ticketCodeFlow = MutableSharedFlow<Result<TicketCode>>()

private val _uiState: MutableStateFlow<TicketEntryUiState> =
MutableStateFlow(TicketEntryUiState.Loading)
val uiState: StateFlow<TicketEntryUiState> = _uiState.asStateFlow()

private val timer: Timer = Timer()

fun loadTicketCode(ticketId: Long) {
init {
viewModelScope.launch {
ticketRepository.loadTicketCode(ticketId)
.onSuccess {
val state = uiState.value
if (state is TicketEntryUiState.Success) {
_uiState.value = state.copy(ticketCode = it, remainTime = it.period)
setTimer(ticketId, it)
}
}.onFailure {
_uiState.value = TicketEntryUiState.Error
analyticsHelper.logNetworkFailure(
key = KEY_LOAD_CODE_LOG,
value = it.message.toString(),
combine(ticketFlow, ticketCodeFlow) { ticketResult, ticketCodeResult ->
runCatching {
val ticket = ticketResult.getOrThrowWithLog()
val ticketCode = ticketCodeResult.getOrThrowWithLog()

setTimer(ticket.id, ticketCode)

TicketEntryUiState.Success(
ticket = ticket,
ticketCode = ticketCode,
remainTime = ticketCode.period,
)
}
}.getOrElse { TicketEntryUiState.Error }
}.collectLatest { _uiState.value = it }
}
}

fun loadTicketCode(ticketId: Long) {
viewModelScope.launch {
ticketCodeFlow.emit(ticketRepository.loadTicketCode(ticketId))
}
}

fun loadTicket(ticketId: Long) {
viewModelScope.launch {
_uiState.value = TicketEntryUiState.Loading
ticketRepository.loadTicket(ticketId)
.onSuccess { ticket ->
ticketRepository.loadTicketCode(ticketId)
.onSuccess { ticketCode ->
_uiState.value = TicketEntryUiState.Success(
ticket = ticket,
ticketCode = ticketCode,
remainTime = ticketCode.period,
)
setTimer(ticketId, ticketCode)
}.onFailure {
_uiState.value = TicketEntryUiState.Error
analyticsHelper.logNetworkFailure(
key = KEY_LOAD_CODE_LOG,
value = it.message.toString(),
)
}
}.onFailure {
_uiState.value = TicketEntryUiState.Error
analyticsHelper.logNetworkFailure(
key = KEY_LOAD_Ticket_LOG,
value = it.message.toString(),
)
}
ticketFlow.emit(ticketRepository.loadTicket(ticketId))
}
}

private suspend fun setTimer(ticketId: Long, ticketCode: TicketCode) {
timer.timerListener = createTimerListener(
ticketId = ticketId,
period = ticketCode.period,
)
timer.timerListener = createTimerListener(ticketId)
timer.start(ticketCode.period)
}

private fun createTimerListener(ticketId: Long, period: Int): TimerListener =
object : TimerListener {
override fun onTick(current: Int) {
val state = uiState.value
if (state is TicketEntryUiState.Success) {
_uiState.value = state.copy(remainTime = current)
}
private fun createTimerListener(ticketId: Long): TimerListener = object : TimerListener {
override fun onTick(current: Int) {
val state = uiState.value
if (state is TicketEntryUiState.Success) {
_uiState.value = state.copy(remainTime = current)
}
}

override fun onFinish() {
viewModelScope.launch {
timer.start(period)
loadTicketCode(ticketId)
}
}
override fun onFinish() {
loadTicketCode(ticketId)
}
}

private fun Result<Ticket>.getOrThrowWithLog(): Ticket = getOrElse { throwable ->
analyticsHelper.logNetworkFailure(
key = KEY_LOAD_Ticket_LOG,
value = throwable.message.toString(),
)

throw throwable
}

private fun Result<TicketCode>.getOrThrowWithLog(): TicketCode = getOrElse { throwable ->
analyticsHelper.logNetworkFailure(
key = KEY_LOAD_CODE_LOG,
value = throwable.message.toString(),
)
throw throwable
}

companion object {
private const val KEY_LOAD_Ticket_LOG = "load_ticket"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.festago.festago.presentation.ui.signin

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.festago.festago.analytics.AnalyticsHelper
import com.festago.festago.repository.AuthRepository
import io.mockk.coEvery
Expand All @@ -9,21 +9,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class SignInViewModelTest {
private lateinit var vm: SignInViewModel
private lateinit var authRepository: AuthRepository
private lateinit var analyticsHelper: AnalyticsHelper

@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()

@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
Expand All @@ -40,49 +37,48 @@ class SignInViewModelTest {
Dispatchers.resetMain()
}

@Test
fun `로그인 성공하면 성공 이벤트가 발생한다`() {
// given
coEvery { authRepository.signIn(any(), any()) } answers { Result.success(Unit) }

// when
vm.signIn("testToken")

// then
assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.SignInSuccess::class.java)
private fun `로그인 결과가 다음과 같을 때`(result: Result<Unit>) {
coEvery { authRepository.signIn(any(), any()) } returns result
}

@Test
fun `로그인 실패하면 실패 이벤트가 발생한다`() {
fun `로그인 성공하면 성공 이벤트가 발생한다`() = runTest {
// given
coEvery {
authRepository.signIn(any(), any())
} answers { Result.failure(Exception()) }
`로그인 결과가 다음과 같을 때`(Result.success(Unit))

// when
vm.signIn("testToken")
vm.event.test {
// when
vm.signIn("testToken")

// then
assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.SignInFailure::class.java)
// then
assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.SignInSuccess::class.java)
}
}

@Test
fun `로그인을 요청하면 로그인 화면을 보여주는 이벤트가 발생한다`() {
fun `로그인 실패하면 실패 이벤트가 발생한다`() = runTest {
// given
// when
vm.signInKakao()
`로그인 결과가 다음과 같을 때`(Result.failure(Exception()))

vm.event.test {
// when
vm.signIn("testToken")

// then
assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.ShowSignInPage::class.java)
// then
assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.SignInFailure::class.java)
}
}

@Test
fun `FCM 토큰을 불러오지 못하면 실패 이벤트가 발생한다`() {
fun `로그인을 요청하면 로그인 화면을 보여주는 이벤트가 발생한다`() = runTest {
// given
// when
vm.signIn("testToken")

// then
assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.SignInFailure::class.java)
vm.event.test {
// when
vm.signInKakao()

// then
assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.ShowSignInPage::class.java)
}
}
}
Loading

0 comments on commit 31cfe5e

Please sign in to comment.