Skip to content

Commit 7f07cf6

Browse files
committed
[BOOK-161] feat: 내서재 동적 리스트 API 연동 및 리스트 바인딩
1 parent fc0f0de commit 7f07cf6

File tree

7 files changed

+262
-112
lines changed

7 files changed

+262
-112
lines changed

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ package com.ninecraft.booket.feature.library
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.getValue
5+
import androidx.compose.runtime.mutableIntStateOf
56
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.rememberCoroutineScope
68
import androidx.compose.runtime.setValue
9+
import com.ninecraft.booket.core.data.api.repository.BookRepository
10+
import com.ninecraft.booket.core.model.LibraryBookContentModel
11+
import com.ninecraft.booket.core.ui.component.FooterState
712
import com.ninecraft.booket.feature.screens.LibraryScreen
813
import com.ninecraft.booket.feature.screens.SettingsScreen
14+
import com.orhanobut.logger.Logger
915
import com.slack.circuit.codegen.annotations.CircuitInject
1016
import com.slack.circuit.retained.rememberRetained
1117
import com.slack.circuit.runtime.Navigator
@@ -16,40 +22,77 @@ import dagger.assisted.AssistedInject
1622
import dagger.hilt.android.components.ActivityRetainedComponent
1723
import kotlinx.collections.immutable.persistentListOf
1824
import kotlinx.collections.immutable.toPersistentList
25+
import kotlinx.coroutines.launch
1926

2027
class LibraryPresenter @AssistedInject constructor(
2128
@Assisted private val navigator: Navigator,
29+
private val repository: BookRepository,
2230
) : Presenter<LibraryUiState> {
31+
companion object {
32+
private const val PAGE_SIZE = 10
33+
private const val START_INDEX = 0
34+
}
2335

2436
@Composable
2537
override fun present(): LibraryUiState {
26-
var isLoading by rememberRetained { mutableStateOf(false) }
38+
val scope = rememberCoroutineScope()
39+
40+
var uiState by rememberRetained { mutableStateOf<UiState>(UiState.Idle) }
41+
var footerState by rememberRetained { mutableStateOf<FooterState>(FooterState.Idle) }
42+
var filterChips by rememberRetained {
43+
mutableStateOf(LibraryFilterOption.entries.map { LibraryFilterChip(option = it, count = 0) }.toPersistentList())
44+
}
45+
var currentFilter by rememberRetained { mutableStateOf(LibraryFilterOption.TOTAL) }
46+
var books by rememberRetained { mutableStateOf(persistentListOf<LibraryBookContentModel>()) }
2747
var sideEffect by rememberRetained { mutableStateOf<LibrarySideEffect?>(null) }
28-
var chipElements by rememberRetained {
29-
mutableStateOf(
30-
persistentListOf(
31-
FilterChipState(
32-
title = BookStatus.TOTAL,
33-
count = 10,
34-
isSelected = true,
35-
),
36-
FilterChipState(
37-
title = BookStatus.BEFORE_READING,
38-
count = 15,
39-
isSelected = false,
40-
),
41-
FilterChipState(
42-
title = BookStatus.READING,
43-
count = 2,
44-
isSelected = false,
45-
),
46-
FilterChipState(
47-
title = BookStatus.COMPLETED,
48-
count = 5,
49-
isSelected = false,
50-
),
51-
),
52-
)
48+
49+
var currentPage by rememberRetained { mutableIntStateOf(START_INDEX) }
50+
var isLastPage by rememberRetained { mutableStateOf(false) }
51+
52+
fun getLibraryBooks(status: String?, page: Int, size: Int) {
53+
scope.launch {
54+
if (page == START_INDEX) {
55+
uiState = UiState.Loading
56+
} else {
57+
footerState = FooterState.Loading
58+
}
59+
60+
repository.getLibrary(status = status, page = page, size = size)
61+
.onSuccess { result ->
62+
filterChips = filterChips.map { chip ->
63+
when (chip.option) {
64+
LibraryFilterOption.TOTAL -> chip.copy(count = result.totalCount)
65+
LibraryFilterOption.BEFORE_READING -> chip.copy(count = result.beforeReadingCount)
66+
LibraryFilterOption.READING -> chip.copy(count = result.readingCount)
67+
LibraryFilterOption.COMPLETED -> chip.copy(count = result.completedCount)
68+
}
69+
}.toPersistentList()
70+
71+
books = if (result.books.page.number == START_INDEX) {
72+
result.books.content.toPersistentList()
73+
} else {
74+
(books + result.books.content).toPersistentList()
75+
}
76+
77+
currentPage = page
78+
isLastPage = result.books.page.number == result.books.page.totalPages - 1
79+
80+
if (page == START_INDEX) {
81+
uiState = UiState.Success
82+
} else {
83+
footerState = if (isLastPage) FooterState.End else FooterState.Idle
84+
}
85+
}
86+
.onFailure { exception ->
87+
Logger.d(exception)
88+
val errorMessage = exception.message ?: "알 수 없는 오류가 발생했습니다."
89+
if (page == START_INDEX) {
90+
uiState = UiState.Error(errorMessage)
91+
} else {
92+
footerState = FooterState.Error(errorMessage)
93+
}
94+
}
95+
}
5396
}
5497

5598
fun handleEvent(event: LibraryUiEvent) {
@@ -63,21 +106,36 @@ class LibraryPresenter @AssistedInject constructor(
63106
}
64107

65108
is LibraryUiEvent.OnFilterClick -> {
66-
chipElements = chipElements.map {
67-
if (it.title == event.bookStatus) {
68-
it.copy(isSelected = true)
69-
} else {
70-
it.copy(isSelected = false)
71-
}
72-
}.toPersistentList()
73-
// TODO: 필터에 해당하는 도서 목록을 불러오는 로직이 들어가야 함
109+
currentFilter = event.filterOption
110+
getLibraryBooks(status = currentFilter.getApiValue(), page = START_INDEX, size = PAGE_SIZE)
111+
}
112+
113+
is LibraryUiEvent.OnBookClick -> {
114+
// TODO: 상세 화면으로 이동
115+
}
116+
117+
is LibraryUiEvent.OnLoadMore -> {
118+
if (footerState !is FooterState.Loading && !isLastPage) {
119+
getLibraryBooks(status = currentFilter.getApiValue(), page = currentPage + 1, size = PAGE_SIZE)
120+
}
121+
}
122+
123+
is LibraryUiEvent.OnRetryClick -> {
124+
if (currentPage == START_INDEX) {
125+
getLibraryBooks(status = currentFilter.getApiValue(), page = currentPage, size = PAGE_SIZE)
126+
} else {
127+
getLibraryBooks(status = currentFilter.getApiValue(), page = currentPage + 1, size = PAGE_SIZE)
128+
}
74129
}
75130
}
76131
}
77132

78133
return LibraryUiState(
79-
isLoading = isLoading,
80-
filterElements = chipElements,
134+
uiState = uiState,
135+
footerState = footerState,
136+
filterChips = filterChips,
137+
currentFilter = currentFilter,
138+
books = books,
81139
sideEffect = sideEffect,
82140
eventSink = ::handleEvent,
83141
)

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
package com.ninecraft.booket.feature.library
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.layout.Arrangement
45
import androidx.compose.foundation.layout.Box
56
import androidx.compose.foundation.layout.Column
67
import androidx.compose.foundation.layout.Spacer
78
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
810
import androidx.compose.foundation.layout.height
11+
import androidx.compose.foundation.lazy.items
12+
import androidx.compose.material3.CircularProgressIndicator
913
import androidx.compose.material3.Text
1014
import androidx.compose.runtime.Composable
1115
import androidx.compose.ui.Alignment
1216
import androidx.compose.ui.Modifier
1317
import androidx.compose.ui.res.stringResource
18+
import androidx.compose.ui.unit.dp
1419
import com.ninecraft.booket.core.designsystem.DevicePreview
20+
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
21+
import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
22+
import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle
1523
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
24+
import com.ninecraft.booket.core.model.LibraryBookContentModel
25+
import com.ninecraft.booket.core.ui.component.InfinityLazyColumn
26+
import com.ninecraft.booket.core.ui.component.LoadStateFooter
1627
import com.ninecraft.booket.feature.library.component.FilterChipGroup
28+
import com.ninecraft.booket.feature.library.component.LibraryBookItem
1729
import com.ninecraft.booket.feature.library.component.LibraryHeader
1830
import com.ninecraft.booket.feature.screens.LibraryScreen
1931
import com.slack.circuit.codegen.annotations.CircuitInject
@@ -62,17 +74,68 @@ internal fun LibraryContent(
6274
},
6375
)
6476
FilterChipGroup(
65-
filterList = state.filterElements,
77+
filterList = state.filterChips,
78+
selectedChipOption = state.currentFilter,
6679
onChipClick = { status ->
6780
state.eventSink(LibraryUiEvent.OnFilterClick(status))
6881
},
6982
)
70-
EmptyBookListScreen()
83+
84+
when (state.uiState) {
85+
is UiState.Idle -> {
86+
EmptyResult()
87+
}
88+
89+
is UiState.Loading -> {
90+
Box(
91+
modifier = Modifier.fillMaxSize(),
92+
contentAlignment = Alignment.Center,
93+
) {
94+
CircularProgressIndicator(color = ReedTheme.colors.contentBrand)
95+
}
96+
}
97+
98+
is UiState.Success -> {
99+
if (state.books.isEmpty()) {
100+
EmptyResult()
101+
} else {
102+
InfinityLazyColumn(
103+
modifier = Modifier.fillMaxSize(),
104+
loadMore = {
105+
state.eventSink(LibraryUiEvent.OnLoadMore)
106+
},
107+
) {
108+
items(state.books) {
109+
LibraryBookItem(
110+
book = it,
111+
onBookClick = {},
112+
)
113+
Box(
114+
modifier = modifier
115+
.fillMaxWidth()
116+
.height(1.dp)
117+
.background(ReedTheme.colors.borderPrimary),
118+
)
119+
}
120+
item {
121+
LoadStateFooter(
122+
footerState = state.footerState,
123+
onRetryClick = { state.eventSink(LibraryUiEvent.OnLoadMore) },
124+
)
125+
}
126+
}
127+
}
128+
}
129+
130+
is UiState.Error -> {
131+
ErrorResult(state.uiState)
132+
}
133+
}
71134
}
72135
}
73136

74137
@Composable
75-
private fun EmptyBookListScreen() {
138+
private fun EmptyResult() {
76139
Box(
77140
modifier = Modifier.fillMaxSize(),
78141
contentAlignment = Alignment.Center,
@@ -95,35 +158,54 @@ private fun EmptyBookListScreen() {
95158
}
96159
}
97160

161+
@Composable
162+
private fun ErrorResult(uiState: UiState.Error) {
163+
Box(
164+
modifier = Modifier.fillMaxSize(),
165+
contentAlignment = Alignment.Center,
166+
) {
167+
Column(
168+
horizontalAlignment = Alignment.CenterHorizontally,
169+
) {
170+
Text(
171+
text = stringResource(R.string.library_error_title),
172+
color = ReedTheme.colors.contentPrimary,
173+
style = ReedTheme.typography.headline1SemiBold,
174+
)
175+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
176+
Text(
177+
text = uiState.message,
178+
color = ReedTheme.colors.contentSecondary,
179+
style = ReedTheme.typography.body1Medium,
180+
)
181+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
182+
ReedButton(
183+
onClick = {
184+
// TODO: 다시 시도
185+
},
186+
sizeStyle = largeButtonStyle,
187+
colorStyle = ReedButtonColorStyle.PRIMARY,
188+
text = stringResource(R.string.library_retry),
189+
)
190+
}
191+
}
192+
}
193+
98194
@DevicePreview
99195
@Composable
100196
private fun LibraryPreview() {
101197
ReedTheme {
102-
val filterList = persistentListOf(
103-
FilterChipState(
104-
title = BookStatus.TOTAL,
105-
count = 10,
106-
isSelected = true,
107-
),
108-
FilterChipState(
109-
title = BookStatus.BEFORE_READING,
110-
count = 15,
111-
isSelected = false,
112-
),
113-
FilterChipState(
114-
title = BookStatus.READING,
115-
count = 2,
116-
isSelected = false,
117-
),
118-
FilterChipState(
119-
title = BookStatus.COMPLETED,
120-
count = 5,
121-
isSelected = false,
122-
),
123-
)
124198
LibraryUi(
125199
state = LibraryUiState(
126-
filterElements = filterList,
200+
uiState = UiState.Success,
201+
books = persistentListOf(
202+
LibraryBookContentModel(
203+
bookTitle = "코틀린을 활용한 안드로이드 프로그래밍",
204+
bookAuthor = "우재남, 유혜림",
205+
coverImageUrl = "https://image.aladin.co.kr/product/24342/42/coversum/k542630705_1.jpg",
206+
publisher = "한빛아카데미",
207+
),
208+
),
127209
eventSink = {},
128210
),
129211
)

0 commit comments

Comments
 (0)