Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.daedan.festabook.presentation.common.component

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.daedan.festabook.R
import com.daedan.festabook.presentation.theme.FestabookTypography
import com.daedan.festabook.presentation.theme.festabookSpacing

@Composable
fun ErrorStateScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(R.drawable.ic_fail_load),
contentDescription = stringResource(R.string.content_description_iv_fail_load),
modifier = Modifier.size(48.dp),
)
Spacer(modifier = Modifier.height(festabookSpacing.paddingBody2))
Text(
text = stringResource(R.string.error_fail_to_load_info),
style = FestabookTypography.bodyLarge,
)
}
}

@Preview(showBackground = true)
@Composable
private fun ErrorStateScreenPreview() {
ErrorStateScreen()
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import com.airbnb.lottie.compose.rememberLottieComposition
import com.daedan.festabook.R

@Composable
fun LoadingStateScreen(modifier: Modifier = Modifier) {
fun LoadingStateScreen(
modifier: Modifier = Modifier,
isPlaying: Boolean = true,
) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading))
val progress by animateLottieCompositionAsState(
composition = composition,
iterations = LottieConstants.IterateForever,
isPlaying = isPlaying,
)
LottieAnimation(
composition = composition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.daedan.festabook.R
import com.daedan.festabook.domain.model.Festival
import com.daedan.festabook.domain.model.Organization
import com.daedan.festabook.domain.model.Poster
Expand Down Expand Up @@ -64,7 +66,7 @@ fun HomeScreen(
is FestivalUiState.Error -> {
Box(modifier = modifier.fillMaxSize()) {
Text(
text = "데이터를 불러오는데 실패했습니다.",
text = stringResource(R.string.error_fail_to_load_info),
modifier = Modifier.align(Alignment.Center),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ package com.daedan.festabook.presentation.schedule

import com.daedan.festabook.presentation.schedule.model.ScheduleEventUiModel

sealed interface ScheduleEventsUiState {
data object InitialLoading : ScheduleEventsUiState
data class ScheduleEventsUiState(
val content: Content,
val isRefreshing: Boolean = false,
) {
sealed interface Content {
data object InitialLoading : Content

data class Refreshing(
val oldEvents: List<ScheduleEventUiModel>,
) : ScheduleEventsUiState
data class Success(
val events: List<ScheduleEventUiModel>,
val currentEventPosition: Int,
) : Content {
val isEventsEmpty get() = events.isEmpty()
}

data class Success(
val events: List<ScheduleEventUiModel>,
val currentEventPosition: Int,
) : ScheduleEventsUiState

data class Error(
val throwable: Throwable,
) : ScheduleEventsUiState
data class Error(
val throwable: Throwable,
) : Content
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ package com.daedan.festabook.presentation.schedule

import com.daedan.festabook.presentation.schedule.model.ScheduleDateUiModel

sealed interface ScheduleUiState {
data object InitialLoading : ScheduleUiState
data class ScheduleUiState(
val content: Content,
) {
sealed interface Content {
data object InitialLoading : Content

data class Refreshing(
val lastSuccessState: Success,
) : ScheduleUiState
data class Success(
val dates: List<ScheduleDateUiModel>,
val currentDatePosition: Int,
val eventsUiStateByPosition: Map<Int, ScheduleEventsUiState> = emptyMap(),
Comment on lines +8 to +14
Copy link
Contributor

Choose a reason for hiding this comment

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

Content를 ScheduleUiState에 중첩하신 이유가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

새로고침을 할 때 새로고침 시간동안 기존 뷰를 유지하려면 기존 refreshing 객체는 이전 상태를 가지고 있어야 했습니다.
하지만 새로 고침 상태에서 이전상태가 성공인지, 에러인지를 또 분기처리를 해서 구현하는 것이 코드를 읽는 입장에서 가독성이 매우 떨어진다고 생각했습니다!

따라서 uiState에서 항상 뷰에 대한 내용을 가지고 있게 하기 위해 data class로 구현 후 Content를 넣어주었습니다.

) : Content

data class Success(
val dates: List<ScheduleDateUiModel>,
val currentDatePosition: Int,
val eventsUiStateByPosition: Map<Int, ScheduleEventsUiState> = emptyMap(),
) : ScheduleUiState

data class Error(
val throwable: Throwable,
) : ScheduleUiState
data class Error(
val throwable: Throwable,
) : Content
}

companion object {
const val DEFAULT_POSITION: Int = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import java.time.LocalDate

@ContributesIntoMap(AppScope::class)
Expand All @@ -26,16 +25,18 @@ class ScheduleViewModel(
private val scheduleRepository: ScheduleRepository,
) : ViewModel() {
private val _scheduleUiState: MutableStateFlow<ScheduleUiState> =
MutableStateFlow(ScheduleUiState.InitialLoading)
MutableStateFlow(
ScheduleUiState(content = ScheduleUiState.Content.InitialLoading),
)
val scheduleUiState: StateFlow<ScheduleUiState> = _scheduleUiState.asStateFlow()

init {
loadSchedules()
}

fun loadSchedules(
scheduleUiState: ScheduleUiState = ScheduleUiState.InitialLoading,
scheduleEventUiState: ScheduleEventsUiState = ScheduleEventsUiState.InitialLoading,
scheduleUiState: ScheduleUiState = ScheduleUiState(content = ScheduleUiState.Content.InitialLoading),
scheduleEventUiState: ScheduleEventsUiState = ScheduleEventsUiState(ScheduleEventsUiState.Content.InitialLoading),
selectedDatePosition: Int? = null,
preloadCount: Int = PRELOAD_PAGE_COUNT,
) {
Expand All @@ -44,7 +45,7 @@ class ScheduleViewModel(

if (datesResult.isSuccess) {
val currentPosition =
(_scheduleUiState.value as ScheduleUiState.Success).currentDatePosition
(_scheduleUiState.value.content as ScheduleUiState.Content.Success).currentDatePosition
loadEventsInRange(currentPosition, scheduleEventUiState, preloadCount)
}
}
Expand All @@ -64,45 +65,47 @@ class ScheduleViewModel(
selectedDatePosition ?: getCurrentDatePosition(scheduleDates)

_scheduleUiState.value =
ScheduleUiState.Success(
dates = scheduleDateUiModels,
currentDatePosition = currentDatePosition,
ScheduleUiState(
content =
ScheduleUiState.Content.Success(
dates = scheduleDateUiModels,
currentDatePosition = currentDatePosition,
),
)

Result.success(scheduleDateUiModels)
},
onFailure = { throwable ->
_scheduleUiState.value = ScheduleUiState.Error(throwable)
_scheduleUiState.value =
ScheduleUiState(content = ScheduleUiState.Content.Error(throwable))
Result.failure(throwable)
},
)
}

fun loadEventsInRange(
currentPosition: Int,
scheduleEventUiState: ScheduleEventsUiState = ScheduleEventsUiState.InitialLoading,
scheduleEventUiState: ScheduleEventsUiState = ScheduleEventsUiState(ScheduleEventsUiState.Content.InitialLoading),
preloadCount: Int = PRELOAD_PAGE_COUNT,
) {
(_scheduleUiState.value as? ScheduleUiState.Success)?.dates?.let { scheduleDates ->
(_scheduleUiState.value.content as? ScheduleUiState.Content.Success)?.dates?.let { scheduleDates ->
val range =
getPreloadRange(
totalPageSize = scheduleDates.size,
currentPosition = currentPosition,
preloadCount = preloadCount,
)
viewModelScope.launch {
supervisorScope {
range.forEach { position ->
if (isEventLoaded(position)) return@forEach

val scheduleDateUiModel = scheduleDates[position]
launch {
loadEventsByPosition(
position = position,
scheduleDateUiModel = scheduleDateUiModel,
scheduleEventsUiState = scheduleEventUiState,
)
}
range.forEach { position ->
if (isEventLoaded(position)) return@forEach

val scheduleDateUiModel = scheduleDates[position]
launch {
loadEventsByPosition(
position = position,
scheduleDateUiModel = scheduleDateUiModel,
scheduleEventsUiState = scheduleEventUiState,
)
}
}
}
Expand All @@ -125,27 +128,38 @@ class ScheduleViewModel(
updateEventUiState(
position = position,
scheduleEventsUiState =
ScheduleEventsUiState.Success(
events = uiModels,
currentEventPosition = getCurrentEventPosition(uiModels),
ScheduleEventsUiState(
content =
ScheduleEventsUiState.Content.Success(
events = uiModels,
currentEventPosition = getCurrentEventPosition(uiModels),
),
),
)
}.onFailure {
updateEventUiState(position, ScheduleEventsUiState.Error(it))
updateEventUiState(
position,
ScheduleEventsUiState(
content = ScheduleEventsUiState.Content.Error(it),
),
)
}
}

private fun updateEventUiState(
position: Int,
scheduleEventsUiState: ScheduleEventsUiState,
) {
val currentUiState = _scheduleUiState.value
if (currentUiState !is ScheduleUiState.Success) return
val currentState = _scheduleUiState.value
val content = currentState.content as? ScheduleUiState.Content.Success ?: return

_scheduleUiState.value =
currentUiState.copy(
eventsUiStateByPosition =
currentUiState.eventsUiStateByPosition + (position to scheduleEventsUiState),
currentState.copy(
content =
content
.copy(
eventsUiStateByPosition = content.eventsUiStateByPosition + (position to scheduleEventsUiState),
),
)
}

Expand Down Expand Up @@ -180,9 +194,13 @@ class ScheduleViewModel(
}

private fun isEventLoaded(position: Int): Boolean {
val currentScheduleUiState = _scheduleUiState.value
if (currentScheduleUiState !is ScheduleUiState.Success) return false
return currentScheduleUiState.eventsUiStateByPosition[position] is ScheduleEventsUiState.Success
val currentScheduleUiStateContent = _scheduleUiState.value.content
if (currentScheduleUiStateContent !is ScheduleUiState.Content.Success) return false

val currentScheduleEventsUiStateContent =
currentScheduleUiStateContent.eventsUiStateByPosition[position]?.content ?: return false

return currentScheduleEventsUiStateContent is ScheduleEventsUiState.Content.Success
}

companion object {
Expand Down

This file was deleted.

Loading