이번 토이 프로젝트에서 처음으로 Paging3와 Compose를 함께 사용하면서 부딪힌 이슈상황과
이를 해결하는 과정에 대해서 정리해보고자 Post를 적게 되었어요. 😭
혹시 잘못된 점이 있다면 꼭 댓글 부탁드립니다. 🙏
Paging3를 이전에도 사용해본적이 있기 때문에 Domain Layer 단까지는 쉽게 구현할 수 있었습니다.
🌹 Compose와 XML 구별 없이 동일한 방법이예요. :-)
ViewModel에서도 이전과 동일한 방법으로 아래와 같이 코드를 작성하였습니다.
val imagesInfo: Flow<PagingData<ImageUiModel>> =
getImagesInfoUseCase()
.map { it.map { image -> image.toUiModel() } }
.cachedIn(viewModelScope)
다른 점이 있다면 PagingAdapter를 사용하는 XML과 달리
Compose에서는 아래와 같이 Flow를 State로 변경하여 Collect 해주게 됩니다.
val images = viewModel.imagesInfo.collectAsLazyPagingItems()
그 다음으로는 리스트의 상태를 관리해줄 LazyListState도 추가해줍니다.
val listState = rememberLazyListState()
이는 Recomposition이 일어나도 LazyList(LazyColumn, LazyRow 등)의 상태를 그대로 유지하기 위함이예요.👍🏻
이제 이 둘을 LazyList에서 사용하면 Paging3를 이용한 리스트 구현은 끝입니다!
LazyVerticalGrid(
modifier = modifier,
cells = GridCells.Fixed(listType.column),
state = listState
) {
items(count = images.itemCount) { index ->
images[index]?.let {
// Item Compose Component
}
}
}
하지만 바로 여기서 생각치도 못한 이슈가 발생하였습니다! 📌
다른 화면으로 이동했다가 popBackStack으로 다시 현재 화면으로 돌아왔을 때 리스트의 스크롤 위치가 초기화되는 문제였습니다. (두둥! 😱)
다시 현재 화면으로 돌아왔을 때, 이전 스크롤 위치가 아닌 가장 상단 이미지가 보인다면 UX적으로 너~무나 좋지 않은 것 같아
이를 우선적으로 해결해보기로 하였습니다. 🤔
무엇이 문제였을까? 생각해보던 중 리스트의 Scroll 위치를 관리하는 LazyListState에 문제가 있는 것일까하는 의문이 들게 되었습니다.
그러나 rememberLazyListState() 코드를 열어보면
@Composable
fun rememberLazyListState(
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
return rememberSaveable(saver = LazyListState.Saver) {
LazyListState(
initialFirstVisibleItemIndex,
initialFirstVisibleItemScrollOffset
)
}
}
rememberSaveable로 관리되고 있는 것을 확인할 수 있었어요....ㅎㅎ
원인을 찾아 디버깅과 구글링을 반복하던 중
popBackStack 시, PagingData의 itemcount 값이 0으로 한번 초기화 되었다가 다시 item을 가지고 오는 것을 알 수 있었습니다.
어.. 어째서😭
문제는 바로 collectAsLazyPagingItem 내부 코드에 있었습니다.
@Composable
public fun <T : Any> Flow<PagingData<T>>.collectAsLazyPagingItems(): LazyPagingItems<T> {
val lazyPagingItems = remember(this) { LazyPagingItems(this) }
LaunchedEffect(lazyPagingItems) {
lazyPagingItems.collectPagingData()
}
LaunchedEffect(lazyPagingItems) {
lazyPagingItems.collectLoadState()
}
return lazyPagingItems
}
이 LazyPagingItems가 rememberSaveable이 아니여서 popBackStack 시 새로 생성되게 됩니다. 😳
이 때 생성되는 LazyPagingItems 내부에 itemcount 프로퍼티를 보게 되면
var itemSnapshotList by mutableStateOf(
ItemSnapshotList<T>(0, 0, emptyList())
)
private set
/**
* The number of items which can be accessed.
*/
val itemCount: Int get() = itemSnapshotList.size
snapShot의 초기값이 emptyList여서 itemCount의 첫 값은 0이고,
LauncedEffect 내에 lazyPgingItems.collectPagingData() 가 호출된 이후에야 PagingData의 itemcount 값을 set하기 때문에
위에서 봤던 이미지처럼
일단 한번 itemcount가 0이 되는 순간이 발생하게 되는 듯 합니다.
itemcount가 일단 한번 0으로 설정되면 어떤 일이 발생하게 될까요?
LazyVerticalGrid(
modifier = modifier,
cells = GridCells.Fixed(listType.column),
state = listState
) {
items(count = images.itemCount) { index ->
images[index]?.let {
// Item Compose Component
}
}
}
리스트를 그리는 Compose를 다시 한번 보게 된다면 images.itemCount에 맞게 리스트의 item들을 생성하기 때문에
리스트에 있는 모든 item들이 제거된 후 다시 그려지는 작업을 수행하게 됩니다.
이것이 바로 LazyListState가 정상적으로 동작하지 않았던 이유입니다. 🪴
이를 해결하기 위해서는 itemcount가 0이 되었을 때 기존 리스트에 있던 item들의 제거를 피하기 위해 아래와 같은 분기처리가 필요하게 됩니다.🔥
if (images.itemCount != 0) {
LazyVerticalGrid(
modifier = modifier,
cells = GridCells.Fixed(listType.column),
state = listState
) {
items(count = images.itemCount) { index ->
images[index]?.let {
// item compose component
}
}
}
}
결론은 PagingData의 itemCount가 0이 되었을 때 별도의 처리를 해주자!
뭐야 rememberSaveable로 된 것도 만들어줘요. 😭
간단하면서도 제 프로젝트 진행 시간을 많이 뺏어먹었던 Compose에서의 Paging3 이용 과정이었습니다.ㅎㅎ
감사합니다!
'Android > Android' 카테고리의 다른 글
[Android/Compose] 언제 Recomposition이 발생하는 것일까? (0) | 2023.03.26 |
---|---|
[Android] Retrofit의 ThreadPool : 왜 Retrofit은 Dispatcher 변경을 하지 않아도 되는것일까.. (0) | 2023.03.10 |
[공식문서 열어보기] AlarmManger로 정확한 시간에 알림 띄워주자 (0) | 2023.02.13 |
[Hilt] Component와 Scope 무엇이 다른지 디버깅으로 다 찍어보자! (0) | 2023.02.11 |
[Android:Codelab] Dependency Injection and Test Doubles - 5 (0) | 2023.01.26 |