반응형
사진 갤러리에서 날짜 데이터를 추가해야 할 일이 생겼습니다.
Paging3는 PagingData<T>로 데이터가 감싸져 있어서, 내부 데이터를 다룰 수가 없는 불편함이 있는 만큼
insertHeaderItem(), insertFooterItem(), insertSeparator() 같은 내부 함수들을 지원합니다.
이 중 insertSeparator()를 활용하면, 우리가 갤러리 앱에서 볼 수 있는 UI를 쉽게 구현할 수 있습니다.
@CheckResult
@JvmSynthetic
public fun <T : R, R : Any> PagingData<T>.insertSeparators(
terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
generator: suspend (T?, T?) -> R?,
): PagingData<R> {
// This function must be an extension method, as it indirectly imposes a constraint on
// the type of T (because T extends R). Ideally it would be declared not be an
// extension, to make this method discoverable for Java callers, but we need to support
// the common UI model pattern for separators:
// class UiModel
// class ItemModel: UiModel
// class SeparatorModel: UiModel
return PagingData(
flow = flow.insertEventSeparators(terminalSeparatorType, generator),
uiReceiver = uiReceiver,
hintReceiver = hintReceiver
)
}
terminalSeparatorType에는 3가지 옵션이 있는데, 디폴트 파라미터로 기본값 FULLY_COMPLETE 가 설정되어 있습니다.
FULLY_COMPLETE (기본값) | 모든 데이터를 로드한 후에만 Separator 추가 | 완전히 로드된 후 날짜별 헤더를 추가할 때 |
SOURCE_COMPLETE | 현재 불러온 데이터까지만 고려해서 Separator 추가 | 스크롤 중에도 헤더를 추가하고 싶을 때 |
ALWAYS | 데이터가 없어도 Separator를 추가 시도 | 빈 리스트 처리에 활용 가능 |
저는 특별한 옵션이 필요한 케이스가 아니라서 기본값을 사용하게 되었습니다.
우선 sealed class를 사용해서 멀티 뷰 타입을 정의해야겠고
sealed class GalleryItem {
data class DateHeader(val date: String) : GalleryItem()
data class ImageItem(val chat: ChatWithIcon) : GalleryItem()
}
그 다음 뷰모델에서 Paging을 사용하는 UseCase를 통해 insertSeparator()를 활용해주었습니다.
@HiltViewModel
class GalleryViewModel @Inject constructor(
private val getGalleryUseCase: GetGalleryUseCase
): ViewModel() {
fun getImage(chatGroup: String) : Flow<PagingData<GalleryItem>> {
return getGalleryUseCase(chatGroup).map { pagingData ->
pagingData.map { chat ->
GalleryItem.ImageItem(chat)
}.insertSeparators { before, after ->
val beforeDate = chat.date.toLocalDate()
val afterDate = cat.date.toLocalDate()
if (before == null && afterDate != null) {
// 첫 번째 데이터에는 헤더 추가
return@insertSeparators GalleryItem.DateHeader(afterDate.toGalleryHeader())
}
if (beforeDate != null && afterDate != null && beforeDate != afterDate) {
// 날짜가 다르면 헤더 추가
return@insertSeparators GalleryItem.DateHeader(afterDate.toGalleryHeader())
}
// 헤더가 필요하지 않으면 null
null
}
}.cachedIn(viewModelScope)
}
}
if문이 두 번 필요했는데
첫번째는, 맨 앞 데이터에는 날짜 헤더 데이터를 무조건 추가해야했기 때문이고
두번째는, 데이터 사이의 날짜가 다르면 날짜 헤더 데이터를 삽입하기 위한 로직입니다.
그리고 갤러리를 N X 3의 형태로 보여주되, 날짜 헤더는 하나의 row를 전부 차지해야 하기 때문에 아래와 같은 로직도 필요합니다.
@Composable
fun GalleryScreen(viewModel: GalleryViewModel, chatGroup: String) {
val galleryItems = viewModel.getImage(chatGroup).collectAsLazyPagingItems()
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp)
) {
items(
count = galleryItems.itemCount,
key = { index -> galleryItems.peek(index)?.hashCode() ?: index }
) { index ->
val item = galleryItems[index]
when (item) {
is GalleryItem.DateHeaderItem -> {
DateHeader(item.date, Modifier.span(3))
}
is GalleryItem.ImageItem -> {
ImageItem(item.uri, Modifier.span(1))
}
}
}
when (galleryItems.loadState.append) {
is LoadState.Loading -> {
item(span = { GridItemSpan(3) }) {
LoadingIndicator()
}
}
is LoadState.Error -> {
item(span = { GridItemSpan(3) }) {
ErrorMessage((galleryItems.loadState.append as LoadState.Error).error)
}
}
}
}
}
결과적으로 다음과 같은 실행 화면을 기대할 수 있습니다.
Paging3를 사용하면서 얻는 장점과, 단점이 있는데요
다음 글에서 정리해보도록 하겠습니다.
반응형
'[클라이언트] > [Android Kotlin]' 카테고리의 다른 글
Paging3 적절히 판단해서 사용하기 (0) | 2025.03.09 |
---|---|
✓ Built build/app/outputs/flutter-apk/app-debug.apk 무한 로딩 (0) | 2024.11.02 |
[우아한테크코스] 5주차 회고 (1) | 2024.03.23 |
[우아한테크코스] 4주차 회고 (0) | 2024.03.23 |
[우아한테크코스] 3주차 회고 (0) | 2024.03.23 |