Skip to content

Commit c8575ab

Browse files
committed
[fix] #194: 사진 추가 바텀시트에 페이지네이션 적용
- getPhotos() 일회성 호출을 getPhotosFlow() 기반 Paging3로 변경하여 20장 초과 사진도 조회 가능 - 바텀시트가 열릴 때만 페이징 수집하도록 조건부 collectAsLazyPagingItems 적용 - 복사 성공 및 시트 Dismiss 시 _importAlbumFilter 초기화
1 parent 0741485 commit c8575ab

4 files changed

Lines changed: 44 additions & 31 deletions

File tree

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailContract.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ data class AlbumFilterOption(val id: Long?, val title: String, val photoCount: I
1515

1616
data class ImportPhotoState(
1717
val isLoading: Boolean = false,
18-
val photos: ImmutableList<Photo> = persistentListOf(),
1918
val selectedAlbumId: Long? = null,
2019
val selectedPhotoIds: ImmutableSet<Long> = persistentSetOf(),
2120
val isShowAlbumDropdown: Boolean = false,

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import com.neki.android.feature.archive.impl.util.ImageDownloader
6363
import com.neki.android.feature.select_album.api.SelectAlbumAction
6464
import kotlinx.collections.immutable.persistentListOf
6565
import kotlinx.collections.immutable.toImmutableList
66+
import kotlinx.coroutines.flow.Flow
6667
import kotlinx.coroutines.flow.flowOf
6768
import timber.log.Timber
6869

@@ -137,6 +138,7 @@ internal fun AlbumDetailRoute(
137138
AlbumDetailScreen(
138139
uiState = uiState,
139140
pagingItems = pagingItems,
141+
importPhotoPagingData = viewModel.importPhotoPagingData,
140142
onIntent = viewModel.store::onIntent,
141143
)
142144
}
@@ -146,6 +148,7 @@ internal fun AlbumDetailRoute(
146148
internal fun AlbumDetailScreen(
147149
uiState: AlbumDetailState,
148150
pagingItems: LazyPagingItems<Photo>,
151+
importPhotoPagingData: Flow<PagingData<Photo>>,
149152
onIntent: (AlbumDetailIntent) -> Unit = {},
150153
) {
151154
val lazyState = rememberLazyStaggeredGridState()
@@ -259,8 +262,10 @@ internal fun AlbumDetailScreen(
259262
}
260263

261264
if (uiState.isShowImportPhotoBottomSheet) {
265+
val importPagingItems = importPhotoPagingData.collectAsLazyPagingItems()
262266
ImportPhotoBottomSheet(
263267
uiState = uiState.importPhotoState,
268+
pagingItems = importPagingItems,
264269
onIntent = onIntent,
265270
)
266271
}
@@ -373,6 +378,7 @@ private fun AlbumDetailScreenPreview() {
373378
title = "앨범 상세",
374379
),
375380
pagingItems = pagingItems,
381+
importPhotoPagingData = flowOf(PagingData.from(emptyList<Photo>())),
376382
)
377383
}
378384
}
@@ -389,6 +395,7 @@ private fun AlbumDetailScreenEmptyPreview() {
389395
title = "빈 앨범",
390396
),
391397
pagingItems = pagingItems,
398+
importPhotoPagingData = flowOf(PagingData.from(emptyList<Photo>())),
392399
)
393400
}
394401
}
@@ -415,6 +422,7 @@ private fun AlbumDetailScreenFavoritePreview() {
415422
isFavoriteAlbum = true,
416423
),
417424
pagingItems = pagingItems,
425+
importPhotoPagingData = flowOf(PagingData.from(emptyList<Photo>())),
418426
)
419427
}
420428
}
@@ -442,6 +450,7 @@ private fun AlbumDetailScreenSelectModePreview() {
442450
selectedPhotos = persistentListOf(photos[0], photos[2], photos[4]),
443451
),
444452
pagingItems = pagingItems,
453+
importPhotoPagingData = flowOf(PagingData.from(emptyList<Photo>())),
445454
)
446455
}
447456
}
@@ -468,6 +477,7 @@ private fun AlbumDetailScreenOptionPopupPreview() {
468477
isShowOptionPopup = true,
469478
),
470479
pagingItems = pagingItems,
480+
importPhotoPagingData = flowOf(PagingData.from(emptyList<Photo>())),
471481
)
472482
}
473483
}

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailViewModel.kt

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import androidx.paging.map
1111
import com.neki.android.core.dataapi.repository.FolderRepository
1212
import com.neki.android.core.dataapi.repository.PhotoRepository
1313
import com.neki.android.core.domain.usecase.UploadMultiplePhotoUseCase
14-
import com.neki.android.core.model.AlbumPreview
1514
import com.neki.android.core.model.Photo
1615
import com.neki.android.core.ui.MviIntentStore
1716
import com.neki.android.core.ui.mviIntentStore
@@ -25,11 +24,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
2524
import kotlinx.collections.immutable.persistentListOf
2625
import kotlinx.collections.immutable.toImmutableList
2726
import kotlinx.collections.immutable.toImmutableSet
28-
import kotlinx.coroutines.async
29-
import kotlinx.coroutines.awaitAll
27+
import kotlinx.coroutines.ExperimentalCoroutinesApi
3028
import kotlinx.coroutines.flow.Flow
3129
import kotlinx.coroutines.flow.MutableStateFlow
3230
import kotlinx.coroutines.flow.combine
31+
import kotlinx.coroutines.flow.flatMapLatest
3332
import kotlinx.coroutines.flow.update
3433
import kotlinx.coroutines.launch
3534
import timber.log.Timber
@@ -51,6 +50,7 @@ class AlbumDetailViewModel @AssistedInject constructor(
5150

5251
private val deletedPhotoIds = MutableStateFlow<Set<Long>>(emptySet())
5352
private val updatedFavorites = MutableStateFlow<Map<Long, Boolean>>(emptyMap())
53+
private val _importAlbumFilter = MutableStateFlow<Long?>(null)
5454

5555
private val originalPagingData: Flow<PagingData<Photo>> =
5656
if (isFavoriteAlbum) {
@@ -59,6 +59,12 @@ class AlbumDetailViewModel @AssistedInject constructor(
5959
photoRepository.getPhotosFlow(albumId)
6060
}.cachedIn(viewModelScope)
6161

62+
@OptIn(ExperimentalCoroutinesApi::class)
63+
val importPhotoPagingData: Flow<PagingData<Photo>> =
64+
_importAlbumFilter.flatMapLatest { folderId ->
65+
photoRepository.getPhotosFlow(folderId = folderId)
66+
}.cachedIn(viewModelScope)
67+
6268
val photoPagingData: Flow<PagingData<Photo>> = combine(
6369
originalPagingData,
6470
deletedPhotoIds,
@@ -109,11 +115,12 @@ class AlbumDetailViewModel @AssistedInject constructor(
109115

110116
AlbumDetailIntent.DismissImportPhotoBottomSheet -> {
111117
reduce { copy(isShowImportPhotoBottomSheet = false, importPhotoState = ImportPhotoState()) }
118+
_importAlbumFilter.value = null
112119
}
113120

114121
is AlbumDetailIntent.SelectImportAlbum -> {
115122
reduce { copy(importPhotoState = importPhotoState.copy(selectedAlbumId = intent.albumId, isShowAlbumDropdown = false)) }
116-
loadImportPhotos(intent.albumId, reduce)
123+
_importAlbumFilter.value = intent.albumId
117124
}
118125

119126
AlbumDetailIntent.ToggleImportAlbumDropdown -> {
@@ -209,7 +216,7 @@ class AlbumDetailViewModel @AssistedInject constructor(
209216
postSideEffect(AlbumDetailSideEffect.OpenGallery)
210217
} else {
211218
reduce { copy(isShowImportPhotoBottomSheet = true, importPhotoState = ImportPhotoState(currentAlbumId = albumId)) }
212-
loadImportAlbumsAndPhotos(reduce)
219+
loadImportAlbums(reduce)
213220
}
214221
}
215222

@@ -280,29 +287,14 @@ class AlbumDetailViewModel @AssistedInject constructor(
280287
}
281288
}
282289

283-
private fun loadImportAlbumsAndPhotos(reduce: (AlbumDetailState.() -> AlbumDetailState) -> Unit) {
284-
viewModelScope.launch {
285-
var loadedAlbums: List<AlbumPreview> = emptyList()
286-
awaitAll(
287-
async { folderRepository.getFolders().onSuccess { albums -> loadedAlbums = albums } },
288-
async {
289-
photoRepository.getPhotos(folderId = null).onSuccess { photos ->
290-
reduce { copy(importPhotoState = importPhotoState.copy(photos = photos.toImmutableList())) }
291-
}
292-
},
293-
)
294-
val options = buildList {
295-
add(AlbumFilterOption(null, "전체사진", loadedAlbums.sumOf { it.photoCount }))
296-
addAll(loadedAlbums.map { AlbumFilterOption(it.id, it.title, it.photoCount) })
297-
}.toImmutableList()
298-
reduce { copy(importPhotoState = importPhotoState.copy(allAlbumOptions = options)) }
299-
}
300-
}
301-
302-
private fun loadImportPhotos(albumId: Long?, reduce: (AlbumDetailState.() -> AlbumDetailState) -> Unit) {
290+
private fun loadImportAlbums(reduce: (AlbumDetailState.() -> AlbumDetailState) -> Unit) {
303291
viewModelScope.launch {
304-
photoRepository.getPhotos(folderId = albumId).onSuccess { photos ->
305-
reduce { copy(importPhotoState = importPhotoState.copy(photos = photos.toImmutableList())) }
292+
folderRepository.getFolders().onSuccess { albums ->
293+
val options = buildList {
294+
add(AlbumFilterOption(null, "전체사진", albums.sumOf { it.photoCount }))
295+
addAll(albums.map { AlbumFilterOption(it.id, it.title, it.photoCount) })
296+
}.toImmutableList()
297+
reduce { copy(importPhotoState = importPhotoState.copy(allAlbumOptions = options)) }
306298
}
307299
}
308300
}
@@ -319,6 +311,7 @@ class AlbumDetailViewModel @AssistedInject constructor(
319311
targetFolderIds = listOf(albumId),
320312
).onSuccess {
321313
reduce { copy(isShowImportPhotoBottomSheet = false, importPhotoState = ImportPhotoState()) }
314+
_importAlbumFilter.value = null
322315
postSideEffect(AlbumDetailSideEffect.ShowToastMessage("사진을 앨범에 추가했어요"))
323316
postSideEffect(AlbumDetailSideEffect.PhotoImported(albumId))
324317
}.onFailure { e ->

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/ImportPhotoBottomSheet.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import androidx.compose.foundation.layout.statusBarsPadding
1717
import androidx.compose.foundation.layout.width
1818
import androidx.compose.foundation.lazy.grid.GridCells
1919
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
20-
import androidx.compose.foundation.lazy.grid.items
2120
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
2221
import androidx.compose.foundation.shape.RoundedCornerShape
2322
import androidx.compose.material3.ExperimentalMaterial3Api
23+
import androidx.paging.LoadState
24+
import androidx.paging.compose.LazyPagingItems
25+
import androidx.paging.compose.itemKey
2426
import androidx.compose.material3.Icon
2527
import androidx.compose.material3.ModalBottomSheet
2628
import androidx.compose.material3.Text
@@ -50,6 +52,7 @@ import com.neki.android.core.designsystem.button.NekiTextButton
5052
import com.neki.android.core.designsystem.modifier.clickableSingle
5153
import com.neki.android.core.designsystem.modifier.dropdownShadow
5254
import com.neki.android.core.designsystem.ui.theme.NekiTheme
55+
import com.neki.android.core.model.Photo
5356
import com.neki.android.feature.archive.impl.album_detail.AlbumDetailIntent
5457
import com.neki.android.feature.archive.impl.album_detail.AlbumFilterOption
5558
import com.neki.android.feature.archive.impl.album_detail.ImportPhotoState
@@ -59,8 +62,12 @@ import kotlinx.collections.immutable.ImmutableList
5962
@Composable
6063
internal fun ImportPhotoBottomSheet(
6164
uiState: ImportPhotoState,
65+
pagingItems: LazyPagingItems<Photo>,
6266
onIntent: (AlbumDetailIntent) -> Unit,
6367
) {
68+
val isEmpty by remember {
69+
derivedStateOf { pagingItems.itemCount == 0 && pagingItems.loadState.refresh is LoadState.NotLoading }
70+
}
6471
ModalBottomSheet(
6572
modifier = Modifier.statusBarsPadding(),
6673
onDismissRequest = { onIntent(AlbumDetailIntent.DismissImportPhotoBottomSheet) },
@@ -90,7 +97,7 @@ internal fun ImportPhotoBottomSheet(
9097
.weight(1f)
9198
.fillMaxWidth(),
9299
) {
93-
if (uiState.photos.isEmpty()) {
100+
if (isEmpty) {
94101
Column(
95102
modifier = Modifier.align(Alignment.Center),
96103
horizontalAlignment = Alignment.CenterHorizontally,
@@ -122,7 +129,11 @@ internal fun ImportPhotoBottomSheet(
122129
horizontalArrangement = Arrangement.spacedBy(2.dp),
123130
verticalArrangement = Arrangement.spacedBy(2.dp),
124131
) {
125-
items(uiState.photos, key = { it.id }) { photo ->
132+
items(
133+
count = pagingItems.itemCount,
134+
key = pagingItems.itemKey { it.id },
135+
) { index ->
136+
val photo = pagingItems[index] ?: return@items
126137
ImportPhotoGridItem(
127138
photo = photo,
128139
isSelected = photo.id in uiState.selectedPhotoIds,

0 commit comments

Comments
 (0)