From a05a8ba5454fddb8a3c3c2303ab0713386ca0606 Mon Sep 17 00:00:00 2001 From: Sehwan Yun <39579912+l5x5l@users.noreply.github.com> Date: Sun, 22 Sep 2024 15:43:09 +0900 Subject: [PATCH] =?UTF-8?q?[Fix]#71=20feature=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=AC=ED=82=B7=EB=82=B4=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=ED=91=9C=EC=8B=9C=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEATURE] 페이징 관련 공통 클래스 구현 및 테스트 코드 추가 * [REFACTOR] 포킷 상세 화면에서 core:feature모듈에 선언한 SimplePaging을 사용하도록 수정 및 최신/오래된 순 정렬이 적용되지 않던 문제 수정 * [REFACTOR] 홈 화면에서 core:feature모듈에 선언한 SimplePaging을 사용하도록 수정 * [REFACTOR] 알람/포킷추가/링크추가 화면에서 core:feature모듈에 선언한 SimplePaging을 사용하도록 수정 * [REFACTOR] 검색 화면에서 core:feature모듈에 선언한 SimplePaging을 사용하도록 수정 및 최신순/오래된순 정렬이 안되던 문제 수정 * [CHORE] 미사용 코드 제거 * [FIX] 검색 화면 내 lazyColumn items에 key속성 추가 * [FIX] 링크 수정으로 인해 포킷의 링크 개수가 변경되엇을 때, 이를 홈 화면 및 포킷 상세 화면에서 반영하지 못하는 문제 수정 * [CHORE] ktlint 적용 --- core/feature/build.gradle.kts | 14 ++ .../feature/model/paging/PagingLoadResult.kt | 18 ++ .../core/feature/model/paging/PagingSource.kt | 5 + .../core/feature/model/paging/PagingState.kt | 5 + .../core/feature/model/paging/SimplePaging.kt | 118 +++++++++++++ .../core/feature/navigation/args/PokitArg.kt | 5 + .../navigation/args/PokitUpdateEvent.kt | 11 ++ .../pokit/core/feature/ExampleUnitTest.kt | 16 -- .../core/feature/SimplePagingUnitTest.kt | 86 ++++++++++ .../core/feature/model/TestPagingSource.kt | 27 +++ .../com/strayalpaca/addlink/AddLinkScreen.kt | 4 +- .../strayalpaca/addlink/AddLinkViewModel.kt | 35 +++- .../strayalpaca/addlink/paging/PokitPaging.kt | 125 -------------- .../addlink/paging/SimplePaging.kt | 13 -- .../addlink/paging/SimplePagingState.kt | 5 - .../strayalpaca/addpokit/AddPokitScreen.kt | 11 +- .../strayalpaca/addpokit/AddPokitViewModel.kt | 27 ++- .../addpokit/paging/PokitPaging.kt | 124 -------------- .../addpokit/paging/SimplePaging.kt | 13 -- .../addpokit/paging/SimplePagingState.kt | 5 - feature/alarm/build.gradle.kts | 1 + .../java/pokitmons/pokit/alarm/AlarmScreen.kt | 10 +- .../pokitmons/pokit/alarm/AlarmViewModel.kt | 32 +++- .../pokit/alarm/paging/AlarmPaging.kt | 125 -------------- .../pokit/alarm/paging/SimplePaging.kt | 13 -- .../pokit/alarm/paging/SimplePagingState.kt | 5 - .../pokitmons/pokit/home/pokit/PokitScreen.kt | 10 +- .../pokit/home/pokit/PokitViewModel.kt | 122 +++++++------ .../pokitdetail/PokitDetailScreen.kt | 14 +- .../pokitdetail/PokitDetailViewModel.kt | 111 +++++++----- .../pokitdetail/paging/LinkPaging.kt | 136 --------------- .../pokitdetail/paging/PokitPaging.kt | 131 -------------- .../pokitdetail/paging/SimplePaging.kt | 13 -- .../pokitdetail/paging/SimplePagingState.kt | 5 - .../pokitdetail/PokitPagingTest.kt | 72 -------- .../pokitmons/pokit/search/SearchScreen.kt | 8 +- .../pokitmons/pokit/search/SearchViewModel.kt | 76 ++++++--- .../filterbottomsheet/FilterBottomSheet.kt | 4 +- .../FilterBottomSheetContent.kt | 6 +- .../searchitemlist/SearchItemList.kt | 8 +- .../pokit/search/paging/LinkPaging.kt | 160 ------------------ .../pokit/search/paging/PokitPaging.kt | 124 -------------- .../pokit/search/paging/SimplePaging.kt | 13 -- .../pokit/search/paging/SimplePagingState.kt | 5 - 44 files changed, 590 insertions(+), 1281 deletions(-) create mode 100644 core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingLoadResult.kt create mode 100644 core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingSource.kt create mode 100644 core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingState.kt create mode 100644 core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/SimplePaging.kt delete mode 100644 core/feature/src/test/java/pokitmons/pokit/core/feature/ExampleUnitTest.kt create mode 100644 core/feature/src/test/java/pokitmons/pokit/core/feature/SimplePagingUnitTest.kt create mode 100644 core/feature/src/test/java/pokitmons/pokit/core/feature/model/TestPagingSource.kt delete mode 100644 feature/addlink/src/main/java/com/strayalpaca/addlink/paging/PokitPaging.kt delete mode 100644 feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePaging.kt delete mode 100644 feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePagingState.kt delete mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/PokitPaging.kt delete mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePaging.kt delete mode 100644 feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePagingState.kt delete mode 100644 feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/AlarmPaging.kt delete mode 100644 feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePaging.kt delete mode 100644 feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePagingState.kt delete mode 100644 feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt delete mode 100644 feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt delete mode 100644 feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePaging.kt delete mode 100644 feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePagingState.kt delete mode 100644 feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/PokitPagingTest.kt delete mode 100644 feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt delete mode 100644 feature/search/src/main/java/pokitmons/pokit/search/paging/PokitPaging.kt delete mode 100644 feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePaging.kt delete mode 100644 feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePagingState.kt diff --git a/core/feature/build.gradle.kts b/core/feature/build.gradle.kts index 26159256..bccf77b1 100644 --- a/core/feature/build.gradle.kts +++ b/core/feature/build.gradle.kts @@ -5,6 +5,10 @@ plugins { } android { + tasks.withType().configureEach { + useJUnitPlatform() + } + namespace = "pokitmons.pokit.core.feature" compileSdk = 34 @@ -56,4 +60,14 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + // kotest + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.kotlin.reflect) + + // mockk + testImplementation(libs.mockk) + androidTestImplementation(libs.mockk.android) + + implementation(project(":domain")) } diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingLoadResult.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingLoadResult.kt new file mode 100644 index 00000000..e0f57b44 --- /dev/null +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingLoadResult.kt @@ -0,0 +1,18 @@ +package pokitmons.pokit.core.feature.model.paging + +import pokitmons.pokit.domain.commom.PokitResult + +sealed interface PagingLoadResult { + data class Success(val result: List) : PagingLoadResult + data class Error(val errorCode: String) : PagingLoadResult + + companion object { + fun fromPokitResult(pokitResult: PokitResult, mapper: (K) -> List): PagingLoadResult { + return if (pokitResult is PokitResult.Success) { + Success(result = mapper(pokitResult.result)) + } else { + Error(errorCode = (pokitResult as PokitResult.Error).error.code) + } + } + } +} diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingSource.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingSource.kt new file mode 100644 index 00000000..c9b9cd96 --- /dev/null +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingSource.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.core.feature.model.paging + +interface PagingSource { + suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult +} diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingState.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingState.kt new file mode 100644 index 00000000..9adf4445 --- /dev/null +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingState.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.core.feature.model.paging + +enum class PagingState { + IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST +} diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/SimplePaging.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/SimplePaging.kt new file mode 100644 index 00000000..22bb91bf --- /dev/null +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/SimplePaging.kt @@ -0,0 +1,118 @@ +package pokitmons.pokit.core.feature.model.paging + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +class SimplePaging ( + private val pagingSource: PagingSource, + private val getKeyFromItem: (ITEM) -> KEY, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val perPage: Int = 10, + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, +) { + private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + val pagingData: StateFlow> = _pagingData.asStateFlow() + + private val _pagingState: MutableStateFlow = MutableStateFlow(PagingState.IDLE) + val pagingState: StateFlow = _pagingState.asStateFlow() + + private var pagingDataRequestJob: Job? = null + private var currentPageIndex = 0 + + suspend fun refresh() { + pagingDataRequestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { PagingState.LOADING_INIT } + + pagingDataRequestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + + val response = pagingSource.load(pageIndex = currentPageIndex, pageSize = perPage * firstRequestPage) + when (response) { + is PagingLoadResult.Success -> { + val itemList = response.result + applyResponse(itemList, firstRequestPage) + } + is PagingLoadResult.Error -> { + _pagingState.update { PagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { PagingState.FAILURE_INIT } + } + } + } + } + + suspend fun load() { + if (pagingState.value != PagingState.IDLE) return + + pagingDataRequestJob?.cancel() + _pagingState.update { PagingState.LOADING_NEXT } + + pagingDataRequestJob = coroutineScope.launch { + try { + val response = pagingSource.load(pageIndex = currentPageIndex, pageSize = perPage) + when (response) { + is PagingLoadResult.Success -> { + val itemList = response.result + applyResponse(itemList) + } + is PagingLoadResult.Error -> { + _pagingState.update { PagingState.FAILURE_NEXT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { PagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { PagingState.LAST } + } else { + _pagingState.update { PagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + fun clear() { + pagingDataRequestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { PagingState.IDLE } + } + + fun deleteItem(targetItemKey: KEY) { + val currentDataList = _pagingData.value + _pagingData.update { currentDataList.filter { getKeyFromItem(it) != targetItemKey } } + } + + fun modifyItem(targetItem: ITEM) { + val currentDataList = _pagingData.value + + _pagingData.update { + currentDataList.map { item -> + if (getKeyFromItem(targetItem) == getKeyFromItem(item)) { + targetItem + } else { + item + } + } + } + } +} diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitArg.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitArg.kt index 6a67a471..74d3b761 100644 --- a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitArg.kt +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitArg.kt @@ -10,3 +10,8 @@ data class PokitArg( val imageUrl: String, val title: String, ) : Parcelable + +data class LinkCountChangedPokitIds( + val increasedPokitId: Int?, + val decreasedPokitId: Int?, +) diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt index a506e3ec..c516186d 100644 --- a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt @@ -16,6 +16,9 @@ object PokitUpdateEvent { private val _addedPokit = MutableSharedFlow() val addedPokit = _addedPokit.asSharedFlow() + private val _countModifiedPokitIds = MutableSharedFlow() + val countModifiedPokitIds = _countModifiedPokitIds.asSharedFlow() + fun updatePokit(pokitArg: PokitArg) { CoroutineScope(Dispatchers.Default).launch { _updatedPokit.emit(pokitArg) @@ -33,4 +36,12 @@ object PokitUpdateEvent { _addedPokit.emit(pokitArg) } } + + fun updatePokitLinkCount(linkRemovedPokitId: Int? = null, linkAddedPokitId: Int? = null) { + if (linkRemovedPokitId == linkAddedPokitId) return + + CoroutineScope(Dispatchers.Default).launch { + _countModifiedPokitIds.emit(LinkCountChangedPokitIds(increasedPokitId = linkAddedPokitId, decreasedPokitId = linkRemovedPokitId)) + } + } } diff --git a/core/feature/src/test/java/pokitmons/pokit/core/feature/ExampleUnitTest.kt b/core/feature/src/test/java/pokitmons/pokit/core/feature/ExampleUnitTest.kt deleted file mode 100644 index 00cadeb1..00000000 --- a/core/feature/src/test/java/pokitmons/pokit/core/feature/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package pokitmons.pokit.core.feature - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/core/feature/src/test/java/pokitmons/pokit/core/feature/SimplePagingUnitTest.kt b/core/feature/src/test/java/pokitmons/pokit/core/feature/SimplePagingUnitTest.kt new file mode 100644 index 00000000..a6914467 --- /dev/null +++ b/core/feature/src/test/java/pokitmons/pokit/core/feature/SimplePagingUnitTest.kt @@ -0,0 +1,86 @@ +package pokitmons.pokit.core.feature + +import io.kotest.common.ExperimentalKotest +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.core.test.testCoroutineScheduler +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import pokitmons.pokit.core.feature.model.TestPagingSource +import pokitmons.pokit.core.feature.model.paging.PagingState +import pokitmons.pokit.core.feature.model.paging.SimplePaging + +const val FIRST_REQUEST_PAGE_SAMPLE = 3 +const val PAGE_LOAD_TIME = 1000L +const val TOTAL_ITEM_COUNT = 46 + +@OptIn(ExperimentalKotest::class, ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class) +class SimplePagingUnitTest : DescribeSpec({ + + describe("SimplePaging").config(coroutineTestScope = true) { + val coroutineScope = this + val testPaging = SimplePaging( + coroutineScope = coroutineScope, + pagingSource = TestPagingSource( + loadTime = PAGE_LOAD_TIME, + totalItemCount = TOTAL_ITEM_COUNT + ), + getKeyFromItem = { it }, + firstRequestPage = FIRST_REQUEST_PAGE_SAMPLE + ) + + context("새로고침을 한 상황에서") { + it("새로고침 로딩 상태가 되어야 한다.") { + testPaging.refresh() + testPaging.pagingState.value shouldBe PagingState.LOADING_INIT + } + + it("다른 페이지 요청을 무시한다.") { + testPaging.refresh() + testPaging.load() + + coroutineScope.testCoroutineScheduler.advanceTimeBy(5000L) + // testCoroutineScheduler.advanceUntilIdle() + // it 내의 this(coroutineScope)와 전체 describe의 coroutineScope가 서로 다르다! + + val state = testPaging.pagingState.first() + state shouldBe PagingState.IDLE + } + + it("초기화 작업은 수행 가능하다.") { + testPaging.refresh() + testPaging.clear() + + val state = testPaging.pagingState.first() + state shouldBe PagingState.IDLE + } + } + + context("기존 페이지를 로드하던 중 새로고침 요청이 들어온 상황에서") { + it("기존 작업을 무시하고 새로고침을 수행한다.") { + testPaging.load() + testPaging.refresh() + testPaging.pagingState.value shouldBe PagingState.LOADING_INIT + } + } + + context("전체 데이터를 모두 로드한 상황에서") { + testPaging.clear() + testPaging.refresh() + coroutineScope.testCoroutineScheduler.advanceTimeBy(PAGE_LOAD_TIME + 1) + testPaging.load() + coroutineScope.testCoroutineScheduler.advanceTimeBy(PAGE_LOAD_TIME + 1) + testPaging.load() + coroutineScope.testCoroutineScheduler.advanceTimeBy(PAGE_LOAD_TIME + 1) + + it("마지막 상태를 가진다.") { + testPaging.pagingState.value shouldBe PagingState.LAST + } + + it("추가적인 데이터 로드 요청을 무시한다.") { + testPaging.load() + testPaging.pagingState.value shouldBe PagingState.LAST + } + } + } +}) diff --git a/core/feature/src/test/java/pokitmons/pokit/core/feature/model/TestPagingSource.kt b/core/feature/src/test/java/pokitmons/pokit/core/feature/model/TestPagingSource.kt new file mode 100644 index 00000000..8d14f0b8 --- /dev/null +++ b/core/feature/src/test/java/pokitmons/pokit/core/feature/model/TestPagingSource.kt @@ -0,0 +1,27 @@ +package pokitmons.pokit.core.feature.model + +import kotlinx.coroutines.delay +import pokitmons.pokit.core.feature.model.paging.PagingLoadResult +import pokitmons.pokit.core.feature.model.paging.PagingSource +import kotlin.math.min + +class TestPagingSource( + private val loadTime: Long = 1000L, + private val totalItemCount: Int = 30, +) : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + delay(loadTime) + + val firstItemCount = pageIndex * pageSize + 1 + + if (totalItemCount < firstItemCount) { + return PagingLoadResult.Success(emptyList()) + } + + val startIndex = pageIndex * pageSize + val lastIndex = min(((pageIndex + 1) * pageSize), totalItemCount) + + val itemList = (startIndex until lastIndex).map { "${it}번째 아이템" } + return PagingLoadResult.Success(itemList) + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt index f08baec8..731a551b 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt @@ -36,10 +36,10 @@ import com.strayalpaca.addlink.model.AddLinkScreenSideEffect import com.strayalpaca.addlink.model.AddLinkScreenState import com.strayalpaca.addlink.model.ScreenStep import com.strayalpaca.addlink.model.ToastMessageEvent -import com.strayalpaca.addlink.paging.SimplePagingState import com.strayalpaca.addlink.utils.BackPressHandler import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.atom.button.PokitButton import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIcon import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIconPosition @@ -106,7 +106,7 @@ fun AddLinkScreenContainer( } LaunchedEffect(startPaging.value) { - if (startPaging.value && pokitListState == SimplePagingState.IDLE) { + if (startPaging.value && pokitListState == PagingState.IDLE) { viewModel.loadNextPokits() } } diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt index 2c089666..327c3395 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt @@ -9,8 +9,6 @@ import com.strayalpaca.addlink.model.Link import com.strayalpaca.addlink.model.Pokit import com.strayalpaca.addlink.model.ScreenStep import com.strayalpaca.addlink.model.ToastMessageEvent -import com.strayalpaca.addlink.paging.PokitPaging -import com.strayalpaca.addlink.paging.SimplePagingState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -27,6 +25,10 @@ import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container +import pokitmons.pokit.core.feature.model.paging.PagingLoadResult +import pokitmons.pokit.core.feature.model.paging.PagingSource +import pokitmons.pokit.core.feature.model.paging.PagingState +import pokitmons.pokit.core.feature.model.paging.SimplePaging import pokitmons.pokit.core.feature.navigation.args.LinkArg import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent import pokitmons.pokit.core.feature.navigation.args.PokitUpdateEvent @@ -54,15 +56,24 @@ class AddLinkViewModel @Inject constructor( ) : ContainerHost, ViewModel() { override val container: Container = container(AddLinkScreenState()) - private val pokitPaging = PokitPaging( - getPokits = getPokitsUseCase, - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0 + private val pokitPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val response = getPokitsUseCase.getPokits(page = pageIndex, size = pageSize) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainPokits -> domainPokits.map { Pokit.fromDomainPokit(it) } } + ) + } + } + + private val pokitPaging = SimplePaging( + pagingSource = pokitPagingSource, + getKeyFromItem = { pokit -> pokit.id }, + coroutineScope = viewModelScope ) val pokitList: StateFlow> = pokitPaging.pagingData - val pokitListState: StateFlow = pokitPaging.pagingState + val pokitListState: StateFlow = pokitPaging.pagingState private val _linkUrl = MutableStateFlow("") val linkUrl: StateFlow = _linkUrl.asStateFlow() @@ -75,6 +86,9 @@ class AddLinkViewModel @Inject constructor( val currentLinkId: Int? = savedStateHandle.get("link_id")?.toIntOrNull() + // 수정 이전 pokit과 수정 이후 pokit이 다른 경우를 체크하기 위해서만 사용 + private var prevPokitId: Int? = null + init { initPokitAddEventDetector() @@ -136,6 +150,7 @@ class AddLinkViewModel @Inject constructor( step = ScreenStep.IDLE ) } + prevPokitId = responseResult.categoryId _title.update { response.result.title } _memo.update { response.result.memo } _linkUrl.update { response.result.data } @@ -255,6 +270,10 @@ class AddLinkViewModel @Inject constructor( if (isCreate) { LinkUpdateEvent.createSuccess(linkArg) } else { + PokitUpdateEvent.updatePokitLinkCount( + linkAddedPokitId = currentSelectedPokit.id.toIntOrNull(), + linkRemovedPokitId = prevPokitId + ) LinkUpdateEvent.modifySuccess(linkArg) } diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/PokitPaging.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/PokitPaging.kt deleted file mode 100644 index 581f5f91..00000000 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/PokitPaging.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.strayalpaca.addlink.paging - -import com.strayalpaca.addlink.model.Pokit -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase -import kotlin.coroutines.cancellation.CancellationException - -class PokitPaging( - private val getPokits: GetPokitsUseCase, - private val perPage: Int = 10, - private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), - private val initPage: Int = 0, - private val firstRequestPage: Int = 3, - private val showUncategorizedPokit: Boolean = true, -) : SimplePaging { - private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) - override val pagingState: StateFlow = _pagingState.asStateFlow() - - private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) - override val pagingData: StateFlow> = _pagingData.asStateFlow() - private var currentPageIndex = initPage - private var requestJob: Job? = null - - override suspend fun refresh() { - requestJob?.cancel() - - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.LOADING_INIT } - requestJob = coroutineScope.launch { - try { - currentPageIndex = initPage - val response = getPokits.getPokits(size = perPage * firstRequestPage, page = currentPageIndex, filterUncategorized = !showUncategorizedPokit) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList, firstRequestPage) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } - } - - override suspend fun load() { - if (pagingState.value != SimplePagingState.IDLE) return - - requestJob?.cancel() - _pagingState.update { SimplePagingState.LOADING_NEXT } - - requestJob = coroutineScope.launch { - try { - val response = getPokits.getPokits(size = perPage, page = currentPageIndex, filterUncategorized = !showUncategorizedPokit) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } - } - - private fun applyResponse(dataInResponse: List, multiple: Int = 1) { - if (dataInResponse.size < perPage * multiple) { - _pagingState.update { SimplePagingState.LAST } - } else { - _pagingState.update { SimplePagingState.IDLE } - } - _pagingData.update { _pagingData.value + dataInResponse } - currentPageIndex += multiple - } - - override fun clear() { - requestJob?.cancel() - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.IDLE } - } - - override suspend fun deleteItem(item: Pokit) { - val capturedDataList = _pagingData.value - _pagingData.update { capturedDataList.filter { it.id != item.id } } - } - - override suspend fun modifyItem(item: Pokit) { - val capturedDataList = _pagingData.value - val targetPokit = capturedDataList.find { it.id == item.id } ?: return - - _pagingData.update { - capturedDataList.map { pokit -> - if (targetPokit.id == pokit.id) { - item - } else { - pokit - } - } - } - } -} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePaging.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePaging.kt deleted file mode 100644 index e89daa6a..00000000 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePaging.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.strayalpaca.addlink.paging - -import kotlinx.coroutines.flow.Flow - -interface SimplePaging { - val pagingData: Flow> - suspend fun refresh() - suspend fun load() - val pagingState: Flow - suspend fun modifyItem(item: T) - suspend fun deleteItem(item: T) - fun clear() -} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePagingState.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePagingState.kt deleted file mode 100644 index 4c1bfc2c..00000000 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePagingState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.strayalpaca.addlink.paging - -enum class SimplePagingState { - IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST -} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt index aa5029f7..a2c87285 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt @@ -46,9 +46,10 @@ import com.strayalpaca.addpokit.model.AddPokitScreenStep import com.strayalpaca.addpokit.model.AddPokitSideEffect import com.strayalpaca.addpokit.model.Pokit import com.strayalpaca.addpokit.model.PokitImage -import com.strayalpaca.addpokit.paging.SimplePagingState import com.strayalpaca.addpokit.utils.BackPressHandler import org.orbitmvi.orbit.compose.collectSideEffect +import pokitmons.pokit.core.feature.model.NetworkState +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.atom.button.PokitButton import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize import pokitmons.pokit.core.ui.components.block.labeledinput.LabeledInput @@ -122,10 +123,10 @@ fun AddPokitScreen( selectPokitProfileImage: (PokitImage) -> Unit = {}, hideToastMessage: () -> Unit = {}, pokits: List = emptyList(), - pokitsState: SimplePagingState = SimplePagingState.IDLE, + pokitsState: PagingState = PagingState.IDLE, loadPokits: () -> Unit = {}, pokitImages: List = emptyList(), - pokitImagesState: SimplePagingState = SimplePagingState.IDLE, + pokitImagesState: NetworkState = NetworkState.IDLE, ) { Column( modifier = Modifier @@ -228,7 +229,7 @@ fun AddPokitScreen( } LaunchedEffect(startPokitPaging.value) { - if (startPokitPaging.value && pokitsState == SimplePagingState.IDLE) { + if (startPokitPaging.value && pokitsState == PagingState.IDLE) { loadPokits() } } @@ -248,7 +249,7 @@ fun AddPokitScreen( } } - if (pokitImagesState == SimplePagingState.LOADING_INIT) { + if (pokitImagesState == NetworkState.LOADING) { CircularProgressIndicator( modifier = Modifier.width(64.dp), color = PokitTheme.colors.brand, diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt index d032f3bd..e3f5425a 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt @@ -9,8 +9,6 @@ import com.strayalpaca.addpokit.model.AddPokitScreenStep import com.strayalpaca.addpokit.model.AddPokitSideEffect import com.strayalpaca.addpokit.model.Pokit import com.strayalpaca.addpokit.model.PokitImage -import com.strayalpaca.addpokit.paging.PokitPaging -import com.strayalpaca.addpokit.paging.SimplePagingState import com.strayalpaca.addpokit.utils.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -24,6 +22,10 @@ import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container +import pokitmons.pokit.core.feature.model.paging.PagingLoadResult +import pokitmons.pokit.core.feature.model.paging.PagingSource +import pokitmons.pokit.core.feature.model.paging.PagingState +import pokitmons.pokit.core.feature.model.paging.SimplePaging import pokitmons.pokit.core.feature.navigation.args.PokitArg import pokitmons.pokit.core.feature.navigation.args.PokitUpdateEvent import pokitmons.pokit.domain.commom.PokitResult @@ -49,18 +51,27 @@ class AddPokitViewModel @Inject constructor( private val pokitId = savedStateHandle.get("pokit_id")?.toIntOrNull() - private val pokitPaging = PokitPaging( - getPokits = getPokitsUseCase, - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0 + private val pokitPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val response = getPokitsUseCase.getPokits(page = pageIndex, size = pageSize) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainPokits -> domainPokits.map { Pokit.fromDomainPokit(it) } } + ) + } + } + + private val pokitPaging = SimplePaging( + pagingSource = pokitPagingSource, + getKeyFromItem = { pokit -> pokit.id }, + coroutineScope = viewModelScope ) private val _pokitName = MutableStateFlow("") val pokitName: StateFlow = _pokitName.asStateFlow() val pokitList: StateFlow> = pokitPaging.pagingData - val pokitListState: StateFlow = pokitPaging.pagingState + val pokitListState: StateFlow = pokitPaging.pagingState private val _pokitIamges = MutableStateFlow>(emptyList()) val pokitImages: StateFlow> = _pokitIamges.asStateFlow() diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/PokitPaging.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/PokitPaging.kt deleted file mode 100644 index e2661e50..00000000 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/PokitPaging.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.strayalpaca.addpokit.paging - -import com.strayalpaca.addpokit.model.Pokit -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase - -class PokitPaging( - private val getPokits: GetPokitsUseCase, - private val perPage: Int = 10, - private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), - private val initPage: Int = 0, - private val firstRequestPage: Int = 3, -) : SimplePaging { - private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) - override val pagingState: StateFlow = _pagingState.asStateFlow() - - private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) - override val pagingData: StateFlow> = _pagingData.asStateFlow() - private var currentPageIndex = initPage - private var requestJob: Job? = null - - override suspend fun refresh() { - requestJob?.cancel() - - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.LOADING_INIT } - requestJob = coroutineScope.launch { - try { - currentPageIndex = initPage - val response = getPokits.getPokits(size = perPage * firstRequestPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList, firstRequestPage) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } - } - - override suspend fun load() { - if (pagingState.value != SimplePagingState.IDLE) return - - requestJob?.cancel() - _pagingState.update { SimplePagingState.LOADING_NEXT } - - requestJob = coroutineScope.launch { - try { - val response = getPokits.getPokits(size = perPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } - } - - private fun applyResponse(dataInResponse: List, multiple: Int = 1) { - if (dataInResponse.size < perPage * multiple) { - _pagingState.update { SimplePagingState.LAST } - } else { - _pagingState.update { SimplePagingState.IDLE } - } - _pagingData.update { _pagingData.value + dataInResponse } - currentPageIndex += multiple - } - - override fun clear() { - requestJob?.cancel() - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.IDLE } - } - - override suspend fun deleteItem(item: Pokit) { - val capturedDataList = _pagingData.value - _pagingData.update { capturedDataList.filter { it.id != item.id } } - } - - override suspend fun modifyItem(item: Pokit) { - val capturedDataList = _pagingData.value - val targetPokit = capturedDataList.find { it.id == item.id } ?: return - - _pagingData.update { - capturedDataList.map { pokit -> - if (targetPokit.id == pokit.id) { - item - } else { - pokit - } - } - } - } -} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePaging.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePaging.kt deleted file mode 100644 index 510374f3..00000000 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePaging.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.strayalpaca.addpokit.paging - -import kotlinx.coroutines.flow.Flow - -interface SimplePaging { - val pagingData: Flow> - suspend fun refresh() - suspend fun load() - val pagingState: Flow - suspend fun modifyItem(item: T) - suspend fun deleteItem(item: T) - fun clear() -} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePagingState.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePagingState.kt deleted file mode 100644 index 6ac52825..00000000 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePagingState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.strayalpaca.addpokit.paging - -enum class SimplePagingState { - IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST -} diff --git a/feature/alarm/build.gradle.kts b/feature/alarm/build.gradle.kts index 71d3f2d6..e47acf4a 100644 --- a/feature/alarm/build.gradle.kts +++ b/feature/alarm/build.gradle.kts @@ -70,5 +70,6 @@ dependencies { implementation(libs.coil.compose) implementation(project(":core:ui")) + implementation(project(":core:feature")) implementation(project(":domain")) } diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmScreen.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmScreen.kt index 965b327a..a8d00c6b 100644 --- a/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmScreen.kt +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmScreen.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.res.stringResource import pokitmons.pokit.alarm.components.alarmitem.AlarmItem import pokitmons.pokit.alarm.components.toolbar.Toolbar import pokitmons.pokit.alarm.model.Alarm -import pokitmons.pokit.alarm.paging.SimplePagingState +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.template.pooki.Pooki import pokitmons.pokit.core.ui.components.template.pookierror.ErrorPooki @@ -56,7 +56,7 @@ fun AlarmScreen( onClickAlarm: (String) -> Unit = {}, onClickAlarmRemove: (String) -> Unit = {}, alarms: List = emptyList(), - alarmsState: SimplePagingState = SimplePagingState.IDLE, + alarmsState: PagingState = PagingState.IDLE, loadNextAlarms: () -> Unit = {}, refreshAlarms: () -> Unit = {}, ) { @@ -78,20 +78,20 @@ fun AlarmScreen( } LaunchedEffect(startAlarmPaging.value) { - if (startAlarmPaging.value && alarmsState == SimplePagingState.IDLE) { + if (startAlarmPaging.value && alarmsState == PagingState.IDLE) { loadNextAlarms() } } when { - alarmsState == SimplePagingState.LOADING_INIT -> { + alarmsState == PagingState.LOADING_INIT -> { LoadingProgress( modifier = Modifier .fillMaxWidth() .weight(1f) ) } - alarmsState == SimplePagingState.FAILURE_INIT -> { + alarmsState == PagingState.FAILURE_INIT -> { ErrorPooki( modifier = Modifier .fillMaxWidth() diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmViewModel.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmViewModel.kt index 8da4f137..0a375609 100644 --- a/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmViewModel.kt +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmViewModel.kt @@ -6,8 +6,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import pokitmons.pokit.alarm.model.Alarm -import pokitmons.pokit.alarm.paging.AlarmPaging -import pokitmons.pokit.alarm.paging.SimplePagingState +import pokitmons.pokit.core.feature.model.paging.PagingLoadResult +import pokitmons.pokit.core.feature.model.paging.PagingSource +import pokitmons.pokit.core.feature.model.paging.PagingState +import pokitmons.pokit.core.feature.model.paging.SimplePaging import pokitmons.pokit.domain.commom.PokitResult import pokitmons.pokit.domain.usecase.alert.DeleteAlertUseCase import pokitmons.pokit.domain.usecase.alert.GetAlertsUseCase @@ -15,14 +17,28 @@ import javax.inject.Inject @HiltViewModel class AlarmViewModel @Inject constructor( - getAlertsUseCase: GetAlertsUseCase, + private val getAlertsUseCase: GetAlertsUseCase, private val deleteAlertUseCase: DeleteAlertUseCase, ) : ViewModel() { - private val alarmPaging = AlarmPaging(getAlertsUseCase = getAlertsUseCase) + private val alarmPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val response = getAlertsUseCase.getAlerts(page = pageIndex, size = pageSize) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { alerts -> alerts.map { Alarm.fromDomainAlarm(it) } } + ) + } + } + + private val alarmPaging = SimplePaging( + pagingSource = alarmPagingSource, + getKeyFromItem = { alarm -> alarm.id }, + coroutineScope = viewModelScope + ) val alarms: StateFlow> = alarmPaging.pagingData - val alarmsState: StateFlow = alarmPaging.pagingState + val alarmsState: StateFlow = alarmPaging.pagingState init { viewModelScope.launch { @@ -36,9 +52,7 @@ class AlarmViewModel @Inject constructor( val response = deleteAlertUseCase.deleteAlert(id) if (response is PokitResult.Success) { viewModelScope.launch { - alarms.value.find { it.id == alarmId }?.let { targetItem -> - alarmPaging.deleteItem(targetItem) - } + alarmPaging.deleteItem(alarmId) } } } @@ -60,7 +74,7 @@ class AlarmViewModel @Inject constructor( val targetAlarm = alarms.value.find { it.id == alarmId } ?: return viewModelScope.launch { - alarmPaging.modifyItem(item = targetAlarm.copy(read = true)) + alarmPaging.modifyItem(targetItem = targetAlarm.copy(read = true)) } } } diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/AlarmPaging.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/AlarmPaging.kt deleted file mode 100644 index b2d21c1a..00000000 --- a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/AlarmPaging.kt +++ /dev/null @@ -1,125 +0,0 @@ -package pokitmons.pokit.alarm.paging - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import pokitmons.pokit.alarm.model.Alarm -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.usecase.alert.GetAlertsUseCase -import kotlin.coroutines.cancellation.CancellationException - -class AlarmPaging( - private val getAlertsUseCase: GetAlertsUseCase, - private val perPage: Int = 10, - private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), - private val initPage: Int = 0, - private val firstRequestPage: Int = 3, -) : SimplePaging { - private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) - override val pagingData: StateFlow> = _pagingData.asStateFlow() - - private val _pagingState: MutableStateFlow = MutableStateFlow(SimplePagingState.IDLE) - override val pagingState: StateFlow = _pagingState.asStateFlow() - - private var currentPageIndex = initPage - private var requestJob: Job? = null - - override suspend fun refresh() { - requestJob?.cancel() - - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.LOADING_INIT } - requestJob = coroutineScope.launch { - try { - currentPageIndex = initPage - val response = getAlertsUseCase.getAlerts(size = perPage * firstRequestPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val alarms = response.result.map { domainAlarm -> - Alarm.fromDomainAlarm(domainAlarm) - } - applyResponse(alarms, firstRequestPage) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } - } - - override suspend fun load() { - if (pagingState.value != SimplePagingState.IDLE) return - - requestJob?.cancel() - _pagingState.update { SimplePagingState.LOADING_NEXT } - - requestJob = coroutineScope.launch { - try { - val response = getAlertsUseCase.getAlerts(size = perPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val alarms = response.result.map { domainAlarm -> - Alarm.fromDomainAlarm(domainAlarm) - } - applyResponse(alarms) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } - } - - private fun applyResponse(dataInResponse: List, multiple: Int = 1) { - if (dataInResponse.size < perPage * multiple) { - _pagingState.update { SimplePagingState.LAST } - } else { - _pagingState.update { SimplePagingState.IDLE } - } - _pagingData.update { _pagingData.value + dataInResponse } - currentPageIndex += multiple - } - - override fun clear() { - requestJob?.cancel() - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.IDLE } - } - - override suspend fun deleteItem(item: Alarm) { - val capturedDataList = _pagingData.value - _pagingData.update { capturedDataList.filter { it.id != item.id } } - } - - override suspend fun modifyItem(item: Alarm) { - val capturedDataList = _pagingData.value - val targetData = capturedDataList.find { it.id == item.id } ?: return - - _pagingData.update { - capturedDataList.map { data -> - if (targetData.id == data.id) { - item - } else { - data - } - } - } - } -} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePaging.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePaging.kt deleted file mode 100644 index 8062d91c..00000000 --- a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePaging.kt +++ /dev/null @@ -1,13 +0,0 @@ -package pokitmons.pokit.alarm.paging - -import kotlinx.coroutines.flow.Flow - -interface SimplePaging { - val pagingData: Flow> - suspend fun refresh() - suspend fun load() - val pagingState: Flow - suspend fun modifyItem(item: T) - suspend fun deleteItem(item: T) - fun clear() -} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePagingState.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePagingState.kt deleted file mode 100644 index 29aeb08e..00000000 --- a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePagingState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package pokitmons.pokit.alarm.paging - -enum class SimplePagingState { - IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST -} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt index 0716220c..a86edfdf 100644 --- a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import com.strayalpaca.pokitdetail.R import com.strayalpaca.pokitdetail.model.BottomSheetType -import com.strayalpaca.pokitdetail.paging.SimplePagingState +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.block.pokitcard.PokitCard import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet @@ -97,10 +97,10 @@ fun PokitScreen( when (selectedCategory) { is Category.Pokit -> { when { - (pokitsState == SimplePagingState.LOADING_INIT) -> { + (pokitsState == PagingState.LOADING_INIT) -> { LoadingProgress(modifier = Modifier.fillMaxSize()) } - (pokitsState == SimplePagingState.FAILURE_INIT) -> { + (pokitsState == PagingState.FAILURE_INIT) -> { ErrorPooki( modifier = Modifier.fillMaxSize(), title = stringResource(id = coreString.title_error), @@ -144,10 +144,10 @@ fun PokitScreen( is Category.Unclassified -> { when { - (unCategoryLinksState == SimplePagingState.LOADING_INIT) -> { + (unCategoryLinksState == PagingState.LOADING_INIT) -> { LoadingProgress(modifier = Modifier.fillMaxSize()) } - (unCategoryLinksState == SimplePagingState.FAILURE_INIT) -> { + (unCategoryLinksState == PagingState.FAILURE_INIT) -> { ErrorPooki( modifier = Modifier.fillMaxSize(), title = stringResource(id = coreString.title_error), diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt index edbc9059..dfe94ae6 100644 --- a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt @@ -5,8 +5,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.strayalpaca.pokitdetail.model.BottomSheetType import com.strayalpaca.pokitdetail.model.Pokit -import com.strayalpaca.pokitdetail.paging.LinkPaging -import com.strayalpaca.pokitdetail.paging.PokitPaging import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,10 +14,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import pokitmons.pokit.core.feature.flow.MutableEventFlow import pokitmons.pokit.core.feature.flow.asEventFlow +import pokitmons.pokit.core.feature.model.paging.PagingLoadResult +import pokitmons.pokit.core.feature.model.paging.PagingSource +import pokitmons.pokit.core.feature.model.paging.SimplePaging import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent import pokitmons.pokit.core.feature.navigation.args.PokitUpdateEvent import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.model.link.Link import pokitmons.pokit.domain.model.link.LinksSort import pokitmons.pokit.domain.model.pokit.MAX_POKIT_COUNT import pokitmons.pokit.domain.model.pokit.PokitsSort @@ -33,6 +33,7 @@ import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase import pokitmons.pokit.home.model.HomeSideEffect import pokitmons.pokit.home.model.HomeToastMessage import javax.inject.Inject +import kotlin.math.max import com.strayalpaca.pokitdetail.model.Link as DetailLink import pokitmons.pokit.domain.model.pokit.Pokit as DomainPokit @@ -60,7 +61,7 @@ class PokitViewModel @Inject constructor( val isCategoryChanged = (targetLink.pokitId != updatedLink.pokitId.toString()) if (isCategoryChanged) { - linkPaging.deleteItem(targetLink) + linkPaging.deleteItem(targetLink.id) } else { val modifiedLink = targetLink.copy( title = updatedLink.title, @@ -87,9 +88,8 @@ class PokitViewModel @Inject constructor( private fun initLinkRemoveEventDetector() { viewModelScope.launch { - LinkUpdateEvent.removedLink.collectLatest { removedLink -> - val targetLink = linkPaging.pagingData.value.find { it.id == removedLink.toString() } ?: return@collectLatest - linkPaging.deleteItem(targetLink) + LinkUpdateEvent.removedLink.collectLatest { removedLinkId -> + linkPaging.deleteItem(removedLinkId.toString()) } } } @@ -103,13 +103,32 @@ class PokitViewModel @Inject constructor( pokitPaging.modifyItem(modifiedPokit) } } + + viewModelScope.launch { + PokitUpdateEvent.countModifiedPokitIds.collectLatest { linkCountChangedPokitIds -> + linkCountChangedPokitIds.increasedPokitId?.let { linkCountIncreasedPokitId -> + val currentPokit = pokitPaging.pagingData.value.find { it.id == linkCountIncreasedPokitId.toString() } + currentPokit?.let { linkCountIncreasedPokit -> + val increasedLinkCount = linkCountIncreasedPokit.count + 1 + pokitPaging.modifyItem(linkCountIncreasedPokit.copy(count = increasedLinkCount)) + } + } + + linkCountChangedPokitIds.decreasedPokitId?.let { linkCountDecreasedPokitId -> + val currentPokit = pokitPaging.pagingData.value.find { it.id == linkCountDecreasedPokitId.toString() } + currentPokit?.let { linkCountDecreasedPokit -> + val decreasedLinkCount = max(0, linkCountDecreasedPokit.count - 1) + pokitPaging.modifyItem(linkCountDecreasedPokit.copy(count = decreasedLinkCount)) + } + } + } + } } private fun initPokitRemoveEventDetector() { viewModelScope.launch { PokitUpdateEvent.removedPokitId.collectLatest { removedPokitId -> - val targetPokit = pokitPaging.pagingData.value.find { it.id == removedPokitId.toString() } ?: return@collectLatest - pokitPaging.deleteItem(targetPokit) + pokitPaging.deleteItem(removedPokitId.toString()) } } } @@ -134,28 +153,53 @@ class PokitViewModel @Inject constructor( var screenType = mutableStateOf(ScreenType.Pokit) private set - private val pokitPaging = PokitPaging( - getPokits = getPokitsUseCase, - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0 + private val pokitPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val sort = when (pokitsSortOrder.value) { + PokitsSortOrder.Latest -> PokitsSort.RECENT + PokitsSortOrder.Name -> PokitsSort.ALPHABETICAL + } + val response = getPokitsUseCase.getPokits(size = pageSize, page = pageIndex, sort = sort) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainPokits -> domainPokits.map { Pokit.fromDomainPokit(it) } } + ) + } + } + + private val pokitPaging = SimplePaging( + pagingSource = pokitPagingSource, + getKeyFromItem = { pokit -> pokit.id }, + coroutineScope = viewModelScope ) - private val linkPaging = LinkPaging( - getLinks = ::getUncategorizedLinks, - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0, - initCategoryId = 1 + private val linksPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val sort = when (linksSortOrder.value) { + UncategorizedLinksSortOrder.Latest -> LinksSort.RECENT + UncategorizedLinksSortOrder.Older -> LinksSort.OLDER + } + val response = getLinksUseCase.getUncategorizedLinks(size = pageSize, page = pageIndex, sort = sort) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainLinks -> domainLinks.map { DetailLink.fromDomainLink(it) } } + ) + } + } + + private val linkPaging = SimplePaging( + pagingSource = linksPagingSource, + getKeyFromItem = { link -> link.id }, + coroutineScope = viewModelScope ) val pokits: StateFlow> - get() = pokitPaging._pagingData.asStateFlow() + get() = pokitPaging.pagingData val pokitsState = pokitPaging.pagingState val unCategoryLinks: StateFlow> - get() = linkPaging._pagingData.asStateFlow() + get() = linkPaging.pagingData val linksState = linkPaging.pagingState @@ -192,19 +236,6 @@ class PokitViewModel @Inject constructor( fun updatePokitsSortOrder(order: PokitsSortOrder) { pokitsSortOrder.value = order - sortPokits() - } - - private fun sortPokits() { - when (pokitsSortOrder.value) { - is PokitsSortOrder.Name -> { - pokitPaging.changeSort(PokitsSort.ALPHABETICAL) - } - is PokitsSortOrder.Latest -> { - pokitPaging.changeSort(PokitsSort.RECENT) - } - } - viewModelScope.launch { pokitPaging.refresh() } @@ -212,32 +243,11 @@ class PokitViewModel @Inject constructor( fun updateLinksSortOrder(order: UncategorizedLinksSortOrder) { linksSortOrder.value = order - sortUncategorizedLinks() - } - - private fun sortUncategorizedLinks() { - when (linksSortOrder.value) { - is UncategorizedLinksSortOrder.Latest -> { - linkPaging.changeOptions(0, LinksSort.RECENT) - } - is UncategorizedLinksSortOrder.Older -> { - linkPaging.changeOptions(0, LinksSort.OLDER) - } - } - viewModelScope.launch { linkPaging.refresh() } } - private suspend fun getUncategorizedLinks(categoryId: Int, size: Int, page: Int, sort: LinksSort): PokitResult> { - return getLinksUseCase.getUncategorizedLinks( - size = size, - page = page, - sort = sort - ) - } - fun updateScreenType(type: ScreenType) { screenType.value = type } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt index ef9e81dd..978f0fa0 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt @@ -32,8 +32,8 @@ import com.strayalpaca.pokitdetail.model.Filter import com.strayalpaca.pokitdetail.model.Link import com.strayalpaca.pokitdetail.model.Pokit import com.strayalpaca.pokitdetail.model.PokitDetailScreenState -import com.strayalpaca.pokitdetail.paging.SimplePagingState import pokitmons.pokit.core.feature.flow.collectAsEffect +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList @@ -121,9 +121,9 @@ fun PokitDetailScreen( hideLinkDetailBottomSheet: () -> Unit = {}, state: PokitDetailScreenState = PokitDetailScreenState(), linkList: List = emptyList(), - linkListState: SimplePagingState = SimplePagingState.IDLE, + linkListState: PagingState = PagingState.IDLE, pokitList: List = emptyList(), - pokitListState: SimplePagingState = SimplePagingState.IDLE, + pokitListState: PagingState = PagingState.IDLE, onClickLink: (Link) -> Unit = {}, onClickPokitModify: (String) -> Unit = {}, onClickPokitRemove: () -> Unit = {}, @@ -163,20 +163,20 @@ fun PokitDetailScreen( } LaunchedEffect(startLinkPaging.value) { - if (startLinkPaging.value && linkListState == SimplePagingState.IDLE) { + if (startLinkPaging.value && linkListState == PagingState.IDLE) { loadNextLinks() } } when { - (linkListState == SimplePagingState.LOADING_INIT) -> { + (linkListState == PagingState.LOADING_INIT) -> { LoadingProgress( modifier = Modifier .fillMaxWidth() .weight(1f) ) } - (linkListState == SimplePagingState.FAILURE_INIT) -> { + (linkListState == PagingState.FAILURE_INIT) -> { ErrorPooki( modifier = Modifier .fillMaxWidth() @@ -285,7 +285,7 @@ fun PokitDetailScreen( } LaunchedEffect(startPaging.value) { - if (startPaging.value && pokitListState == SimplePagingState.IDLE) { + if (startPaging.value && pokitListState == PagingState.IDLE) { loadNextPokits() } } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt index 98ed8a19..d4705edf 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt @@ -8,9 +8,6 @@ import com.strayalpaca.pokitdetail.model.Filter import com.strayalpaca.pokitdetail.model.Link import com.strayalpaca.pokitdetail.model.Pokit import com.strayalpaca.pokitdetail.model.PokitDetailScreenState -import com.strayalpaca.pokitdetail.paging.LinkPaging -import com.strayalpaca.pokitdetail.paging.PokitPaging -import com.strayalpaca.pokitdetail.paging.SimplePagingState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -20,6 +17,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import pokitmons.pokit.core.feature.flow.MutableEventFlow import pokitmons.pokit.core.feature.flow.asEventFlow +import pokitmons.pokit.core.feature.model.paging.PagingLoadResult +import pokitmons.pokit.core.feature.model.paging.PagingSource +import pokitmons.pokit.core.feature.model.paging.PagingState +import pokitmons.pokit.core.feature.model.paging.SimplePaging import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent import pokitmons.pokit.core.feature.navigation.args.PokitUpdateEvent import pokitmons.pokit.domain.commom.PokitResult @@ -32,7 +33,7 @@ import pokitmons.pokit.domain.usecase.pokit.DeletePokitUseCase import pokitmons.pokit.domain.usecase.pokit.GetPokitUseCase import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase import javax.inject.Inject -import pokitmons.pokit.domain.model.link.Link as DomainLink +import kotlin.math.max @HiltViewModel class PokitDetailViewModel @Inject constructor( @@ -45,29 +46,56 @@ class PokitDetailViewModel @Inject constructor( private val getLinkUseCase: GetLinkUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val pokitPaging = PokitPaging( - getPokits = getPokitsUseCase, - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0 + private val pokitPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val response = getPokitsUseCase.getPokits(size = pageSize, page = pageIndex) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainPokits -> domainPokits.map { Pokit.fromDomainPokit(it) } } + ) + } + } + private val pokitPaging = SimplePaging( + pagingSource = pokitPagingSource, + getKeyFromItem = { pokit -> pokit.id }, + coroutineScope = viewModelScope ) - private val linkPaging = LinkPaging( - getLinks = ::getLinks, - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0, - initCategoryId = 1 + private val linkPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val currentPokit = state.value.currentPokit + val currentFilter = state.value.currentFilter + val categoryId = currentPokit?.id?.toIntOrNull() ?: savedStateHandle.get("pokit_id")?.toIntOrNull() ?: 0 + val sort = if (currentFilter.recentSortUsed) LinksSort.RECENT else LinksSort.OLDER + val response = getLinksUseCase.getLinks( + page = pageIndex, + size = pageSize, + categoryId = categoryId, + sort = sort, + isRead = if (currentFilter.notReadChecked) false else null, + favorite = if (currentFilter.bookmarkChecked) true else null + ) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainLinks -> domainLinks.map { Link.fromDomainLink(it) } } + ) + } + } + + private val linkPaging = SimplePaging( + pagingSource = linkPagingSource, + getKeyFromItem = { link -> link.id }, + coroutineScope = viewModelScope ) private val _state = MutableStateFlow(PokitDetailScreenState()) val state: StateFlow = _state.asStateFlow() val pokitList: StateFlow> = pokitPaging.pagingData - val pokitListState: StateFlow = pokitPaging.pagingState + val pokitListState: StateFlow = pokitPaging.pagingState val linkList: StateFlow> = linkPaging.pagingData - val linkListState: StateFlow = linkPaging.pagingState + val linkListState: StateFlow = linkPaging.pagingState private val _moveToBackEvent = MutableEventFlow() val moveToBackEvent = _moveToBackEvent.asEventFlow() @@ -77,7 +105,6 @@ class PokitDetailViewModel @Inject constructor( val linkCount = savedStateHandle.get("pokit_count")?.toIntOrNull() ?: 0 pokitId?.let { id -> - linkPaging.changeOptions(categoryId = id, sort = LinksSort.RECENT) viewModelScope.launch { linkPaging.refresh() } @@ -93,13 +120,18 @@ class PokitDetailViewModel @Inject constructor( viewModelScope.launch { LinkUpdateEvent.updatedLink.collectLatest { updatedLink -> val targetLink = linkPaging.pagingData.value.find { it.id == updatedLink.id.toString() } ?: return@collectLatest - val modifiedLink = targetLink.copy( - title = updatedLink.title, - imageUrl = updatedLink.thumbnail, - domainUrl = updatedLink.domain, - createdAt = updatedLink.createdAt - ) - linkPaging.modifyItem(modifiedLink) + + if (updatedLink.pokitId.toString() != targetLink.pokitId) { + linkPaging.deleteItem(targetLink.id) + } else { + val modifiedLink = targetLink.copy( + title = updatedLink.title, + imageUrl = updatedLink.thumbnail, + domainUrl = updatedLink.domain, + createdAt = updatedLink.createdAt + ) + linkPaging.modifyItem(modifiedLink) + } } } } @@ -108,7 +140,11 @@ class PokitDetailViewModel @Inject constructor( viewModelScope.launch { LinkUpdateEvent.removedLink.collectLatest { removedLinkId -> val targetLink = linkPaging.pagingData.value.find { it.id == removedLinkId.toString() } ?: return@collectLatest - linkPaging.deleteItem(targetLink) + linkPaging.deleteItem(targetLink.id) + + val currentPokit = state.value.currentPokit ?: return@collectLatest + val changedLinkCount = max(currentPokit.count - 1, 0) + _state.update { it.copy(currentPokit = currentPokit.copy(count = changedLinkCount)) } } } } @@ -124,18 +160,18 @@ class PokitDetailViewModel @Inject constructor( _state.update { it.copy(currentPokit = pokit) } } } - } - private suspend fun getLinks(categoryId: Int, size: Int, page: Int, sort: LinksSort): PokitResult> { - val currentFilter = state.value.currentFilter - return getLinksUseCase.getLinks( - categoryId = categoryId, - size = size, - page = page, - sort = sort, - isRead = if (currentFilter.notReadChecked) false else null, - favorite = if (currentFilter.bookmarkChecked) true else null - ) + viewModelScope.launch { + PokitUpdateEvent.countModifiedPokitIds.collectLatest { linkCountChangedPokitIds -> + val currentPokit = state.value.currentPokit ?: return@collectLatest + linkCountChangedPokitIds.decreasedPokitId?.let { targetId -> + if (targetId.toString() == currentPokit.id) { + val changedLinkCount = max(currentPokit.count - 1, 0) + _state.update { it.copy(currentPokit = currentPokit.copy(count = changedLinkCount)) } + } + } + } + } } private fun getPokit(pokitId: Int, linkCount: Int) { @@ -149,7 +185,6 @@ class PokitDetailViewModel @Inject constructor( fun changePokit(pokit: Pokit) { _state.update { it.copy(currentPokit = pokit, pokitSelectBottomSheetVisible = false) } - linkPaging.changeOptions(categoryId = pokit.id.toInt(), sort = LinksSort.RECENT) viewModelScope.launch { linkPaging.refresh() } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt deleted file mode 100644 index 37662455..00000000 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.strayalpaca.pokitdetail.paging - -import com.strayalpaca.pokitdetail.model.Link -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.model.link.LinksSort -import kotlin.reflect.KSuspendFunction4 -import pokitmons.pokit.domain.model.link.Link as DomainLink - -class LinkPaging( - private var getLinks: KSuspendFunction4>>, - private val perPage: Int = 10, - private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), - private val initPage: Int = 0, - private val firstRequestPage: Int = 3, - initCategoryId: Int = 0, -) : SimplePaging { - private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) - override val pagingState: StateFlow = _pagingState.asStateFlow() - - val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) - - override val pagingData: StateFlow> = _pagingData.asStateFlow() - private var currentPageIndex = initPage - private var requestJob: Job? = null - - private var currentCategoryId: Int = initCategoryId - private var currentSort = LinksSort.RECENT - - fun changeOptions(categoryId: Int, sort: LinksSort) { - currentCategoryId = categoryId - currentSort = sort - } - - override suspend fun refresh() { - requestJob?.cancel() - - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.LOADING_INIT } - requestJob = coroutineScope.launch { - try { - currentPageIndex = initPage - val response = getLinks(currentCategoryId, perPage * firstRequestPage, currentPageIndex, currentSort) - when (response) { - is PokitResult.Success -> { - val links = response.result.map { domainLink -> - Link.fromDomainLink(domainLink) - } - applyResponse(links, firstRequestPage) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } - } - - override suspend fun load() { - if (pagingState.value != SimplePagingState.IDLE) return - - requestJob?.cancel() - _pagingState.update { SimplePagingState.LOADING_NEXT } - - requestJob = coroutineScope.launch { - try { - val response = getLinks(currentCategoryId, perPage, currentPageIndex, currentSort) - when (response) { - is PokitResult.Success -> { - val links = response.result.map { domainLink -> - Link.fromDomainLink(domainLink) - } - applyResponse(links) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } - } - - private fun applyResponse(dataInResponse: List, multiple: Int = 1) { - if (dataInResponse.size < perPage * multiple) { - _pagingState.update { SimplePagingState.LAST } - } else { - _pagingState.update { SimplePagingState.IDLE } - } - _pagingData.update { _pagingData.value + dataInResponse } - currentPageIndex += multiple - } - - override fun clear() { - requestJob?.cancel() - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.IDLE } - } - - override suspend fun deleteItem(item: Link) { - val capturedDataList = _pagingData.value - _pagingData.update { capturedDataList.filter { it.id != item.id } } - } - - override suspend fun modifyItem(item: Link) { - val capturedDataList = _pagingData.value - val targetPokit = capturedDataList.find { it.id == item.id } ?: return - - _pagingData.update { - capturedDataList.map { pokit -> - if (targetPokit.id == pokit.id) { - item - } else { - pokit - } - } - } - } -} diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt deleted file mode 100644 index ca2a8702..00000000 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.strayalpaca.pokitdetail.paging - -import com.strayalpaca.pokitdetail.model.Pokit -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.model.pokit.PokitsSort -import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase - -class PokitPaging( - private val getPokits: GetPokitsUseCase, - private val perPage: Int = 10, - private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), - private val initPage: Int = 0, - private val firstRequestPage: Int = 3, -) : SimplePaging { - private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) - override val pagingState: StateFlow = _pagingState.asStateFlow() - - val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) - - override val pagingData: StateFlow> = _pagingData.asStateFlow() - private var currentPageIndex = initPage - private var requestJob: Job? = null - private var sort: PokitsSort = PokitsSort.RECENT - - override suspend fun refresh() { - requestJob?.cancel() - - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.LOADING_INIT } - requestJob = coroutineScope.launch { - try { - currentPageIndex = initPage - val response = getPokits.getPokits(sort = sort, size = perPage * firstRequestPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList, firstRequestPage) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } - } - - override suspend fun load() { - if (pagingState.value != SimplePagingState.IDLE) return - - requestJob?.cancel() - _pagingState.update { SimplePagingState.LOADING_NEXT } - - requestJob = coroutineScope.launch { - try { - val response = getPokits.getPokits(sort = sort, size = perPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } - } - - private fun applyResponse(dataInResponse: List, multiple: Int = 1) { - if (dataInResponse.size < perPage * multiple) { - _pagingState.update { SimplePagingState.LAST } - } else { - _pagingState.update { SimplePagingState.IDLE } - } - _pagingData.update { _pagingData.value + dataInResponse } - currentPageIndex += multiple - } - - override fun clear() { - requestJob?.cancel() - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.IDLE } - } - - override suspend fun deleteItem(item: Pokit) { - val capturedDataList = _pagingData.value - _pagingData.update { capturedDataList.filter { it.id != item.id } } - } - - override suspend fun modifyItem(item: Pokit) { - val capturedDataList = _pagingData.value - val targetPokit = capturedDataList.find { it.id == item.id } ?: return - - _pagingData.update { - capturedDataList.map { pokit -> - if (targetPokit.id == pokit.id) { - item - } else { - pokit - } - } - } - } - - fun changeSort(sort: PokitsSort) { - this.sort = sort - } -} diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePaging.kt deleted file mode 100644 index de38d66e..00000000 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePaging.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.strayalpaca.pokitdetail.paging - -import kotlinx.coroutines.flow.Flow - -interface SimplePaging { - val pagingData: Flow> - suspend fun refresh() - suspend fun load() - val pagingState: Flow - suspend fun modifyItem(item: T) - suspend fun deleteItem(item: T) - fun clear() -} diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePagingState.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePagingState.kt deleted file mode 100644 index a0fc6519..00000000 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePagingState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.strayalpaca.pokitdetail.paging - -enum class SimplePagingState { - IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST -} diff --git a/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/PokitPagingTest.kt b/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/PokitPagingTest.kt deleted file mode 100644 index 29a7c2cf..00000000 --- a/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/PokitPagingTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.strayalpaca.pokitdetail - -import com.strayalpaca.pokitdetail.paging.PokitPaging -import com.strayalpaca.pokitdetail.paging.SimplePagingState -import io.kotest.common.ExperimentalKotest -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.core.test.testCoroutineScheduler -import io.kotest.matchers.shouldBe -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase -import pokitmons.pokit.domain.model.pokit.Pokit as DomainPokit - -const val PER_PAGE_SAMPLE = 3 -const val FIRST_REQUEST_PAGE_SAMPLE = 3 - -@OptIn(ExperimentalKotest::class, ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class) -class PokitPagingTest : DescribeSpec({ - val sampleGetPokitsUseCase: GetPokitsUseCase = mockk() - - describe("PokitPaging").config(coroutineTestScope = true) { - val coroutineScope = this - val pokitPaging = PokitPaging( - getPokits = sampleGetPokitsUseCase, - coroutineScope = coroutineScope, - firstRequestPage = FIRST_REQUEST_PAGE_SAMPLE, - perPage = PER_PAGE_SAMPLE - ) - coEvery { sampleGetPokitsUseCase.getPokits(size = PER_PAGE_SAMPLE * FIRST_REQUEST_PAGE_SAMPLE, page = 0) } coAnswers { - delay(1000L) - PokitResult.Success(result = listOf(DomainPokit(1, 1, "", DomainPokit.Image(1, ""), 1, ""))) - } - coEvery { sampleGetPokitsUseCase.getPokits(size = PER_PAGE_SAMPLE, page = 0) } coAnswers { - delay(1000L) - PokitResult.Success(result = listOf(DomainPokit(1, 1, "", DomainPokit.Image(1, ""), 1, ""))) - } - - context("새로고침을 하는 경우") { - it("새로고침 로딩 상태가 되어야 한다.") { - pokitPaging.refresh() - pokitPaging.pagingState.value shouldBe SimplePagingState.LOADING_INIT - } - } - - context("기존 페이지를 로드하던 중 다른 페이지 요청이 들어온 경우") { - it("해당 요청을 무시하고 기존 상태를 유지한다.") { - pokitPaging.refresh() - pokitPaging.load() - - coroutineScope.testCoroutineScheduler.advanceTimeBy(5000L) - - val state = pokitPaging.pagingState.first() - state shouldBe SimplePagingState.LAST - - // testCoroutineScheduler.advanceUntilIdle() - // it 내의 this(coroutineScope)와 전체 describe의 coroutineScope가 서로 다르다! - } - } - - context("기존 페이지를 로드하던 중 새로고침 요청이 들어온 경우") { - it("기존 작업을 무시하고 새로고침을 수행한다.") { - pokitPaging.load() - pokitPaging.refresh() - pokitPaging.pagingState.value shouldBe SimplePagingState.LOADING_INIT - } - } - } -}) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt index 0f6de2c0..c7dd4e0d 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet import pokitmons.pokit.core.ui.components.template.linkdetailbottomsheet.LinkDetailBottomSheet @@ -35,7 +36,6 @@ import pokitmons.pokit.search.model.FilterType import pokitmons.pokit.search.model.Link import pokitmons.pokit.search.model.SearchScreenState import pokitmons.pokit.search.model.SearchScreenStep -import pokitmons.pokit.search.paging.SimplePagingState import pokitmons.pokit.core.ui.R.string as coreString @Composable @@ -167,7 +167,7 @@ fun SearchScreen( state: SearchScreenState = SearchScreenState(), currentSearchWord: String = "", linkList: List = emptyList(), - linkPagingState: SimplePagingState = SimplePagingState.IDLE, + linkPagingState: PagingState = PagingState.IDLE, onClickBack: () -> Unit = {}, inputSearchWord: (String) -> Unit = {}, onClickSearch: () -> Unit = {}, @@ -223,14 +223,14 @@ fun SearchScreen( if (state.step == SearchScreenStep.RESULT) { when { - (linkPagingState == SimplePagingState.LOADING_INIT) -> { + (linkPagingState == PagingState.LOADING_INIT) -> { LoadingProgress( modifier = Modifier .fillMaxWidth() .weight(1f) ) } - (linkPagingState == SimplePagingState.FAILURE_INIT) -> { + (linkPagingState == PagingState.FAILURE_INIT) -> { ErrorPooki( modifier = Modifier .fillMaxWidth() diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt index fc205e8a..7835fc76 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt @@ -12,8 +12,13 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import pokitmons.pokit.core.feature.model.paging.PagingLoadResult +import pokitmons.pokit.core.feature.model.paging.PagingSource +import pokitmons.pokit.core.feature.model.paging.PagingState +import pokitmons.pokit.core.feature.model.paging.SimplePaging import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.LinksSort import pokitmons.pokit.domain.usecase.link.DeleteLinkUseCase import pokitmons.pokit.domain.usecase.link.GetLinkUseCase import pokitmons.pokit.domain.usecase.link.SearchLinksUseCase @@ -31,9 +36,6 @@ import pokitmons.pokit.search.model.Link import pokitmons.pokit.search.model.Pokit import pokitmons.pokit.search.model.SearchScreenState import pokitmons.pokit.search.model.SearchScreenStep -import pokitmons.pokit.search.paging.LinkPaging -import pokitmons.pokit.search.paging.PokitPaging -import pokitmons.pokit.search.paging.SimplePagingState import javax.inject.Inject @HiltViewModel @@ -55,26 +57,60 @@ class SearchViewModel @Inject constructor( initLinkRemoveEventDetector() } - private val linkPaging = LinkPaging( - searchLinksUseCase = searchLinksUseCase, - filter = Filter(), - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0 + private val linkPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val currentFilter = state.value.filter ?: Filter() + + val isRead = if (currentFilter.notRead) false else null + val favorites = if (currentFilter.bookmark) true else null + val sort = if (state.value.sortRecent) LinksSort.RECENT else LinksSort.OLDER + val currentAppliedSearchWord = appliedSearchWord + + val response = searchLinksUseCase.searchLinks( + page = pageIndex, + size = pageSize, + sort = listOf(sort.value), + isRead = isRead, + favorites = favorites, + startDate = currentFilter.startDate?.toDateString(), + endDate = currentFilter.endDate?.toDateString(), + categoryIds = currentFilter.selectedPokits.mapNotNull { it.id.toIntOrNull() }, + searchWord = currentAppliedSearchWord + ) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainLinks -> domainLinks.map { Link.fromDomainLink(it) } } + ) + } + } + + private val linkPaging = SimplePaging( + pagingSource = linkPagingSource, + getKeyFromItem = { link -> link.id }, + coroutineScope = viewModelScope ) - private val pokitPaging = PokitPaging( - getPokits = getPokitsUseCase, - perPage = 10, - coroutineScope = viewModelScope, - initPage = 0 + private val pokitPagingSource = object : PagingSource { + override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult { + val response = getPokitsUseCase.getPokits(page = pageIndex, size = pageSize) + return PagingLoadResult.fromPokitResult( + pokitResult = response, + mapper = { domainPokits -> domainPokits.map { Pokit.fromDomainPokit(it) } } + ) + } + } + + private val pokitPaging = SimplePaging( + pagingSource = pokitPagingSource, + getKeyFromItem = { pokit -> pokit.id }, + coroutineScope = viewModelScope ) val linkList: StateFlow> = linkPaging.pagingData - val linkPagingState: StateFlow = linkPaging.pagingState + val linkPagingState: StateFlow = linkPaging.pagingState val pokitList: StateFlow> = pokitPaging.pagingData - val pokitPagingState: StateFlow = pokitPaging.pagingState + val pokitPagingState: StateFlow = pokitPaging.pagingState private val _searchWord = MutableStateFlow("") val searchWord = _searchWord.asStateFlow() @@ -108,7 +144,7 @@ class SearchViewModel @Inject constructor( viewModelScope.launch { LinkUpdateEvent.removedLink.collectLatest { removedLinkId -> val targetItem = linkPaging.pagingData.value.find { it.id == removedLinkId.toString() } ?: return@collectLatest - linkPaging.deleteItem(targetItem) + linkPaging.deleteItem(targetItem.id) } } } @@ -133,7 +169,6 @@ class SearchViewModel @Inject constructor( } viewModelScope.launch { addRecentSearchWordUseCase.addRecentSearchWord(appliedSearchWord) - linkPaging.changeSearchWord(appliedSearchWord) linkPaging.refresh() } } @@ -149,7 +184,6 @@ class SearchViewModel @Inject constructor( } viewModelScope.launch { addRecentSearchWordUseCase.addRecentSearchWord(appliedSearchWord) - linkPaging.changeSearchWord(appliedSearchWord) linkPaging.refresh() } } @@ -272,7 +306,6 @@ class SearchViewModel @Inject constructor( } viewModelScope.launch { - linkPaging.changeFilter(filter) linkPaging.refresh() } } @@ -283,7 +316,6 @@ class SearchViewModel @Inject constructor( } viewModelScope.launch { - linkPaging.changeRecentSort(state.value.sortRecent) linkPaging.refresh() } } @@ -332,7 +364,7 @@ class SearchViewModel @Inject constructor( if (response is PokitResult.Success) { LinkUpdateEvent.removeSuccess(currentLinkId) val targetLink = linkPaging.pagingData.value.find { it.id == currentLinkId.toString() } ?: return@launch - linkPaging.deleteItem(targetLink) + linkPaging.deleteItem(targetLink.id) } } } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt index 2d0540a3..b86bf690 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt @@ -2,12 +2,12 @@ package pokitmons.pokit.search.components.filterbottomsheet import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet import pokitmons.pokit.search.model.Filter import pokitmons.pokit.search.model.FilterType import pokitmons.pokit.search.model.Pokit import pokitmons.pokit.search.model.samplePokits -import pokitmons.pokit.search.paging.SimplePagingState @Composable fun FilterBottomSheet( @@ -15,7 +15,7 @@ fun FilterBottomSheet( firstShowType: FilterType = FilterType.Pokit, onSaveClilck: (Filter) -> Unit = {}, pokits: List = samplePokits, - pokitPagingState: SimplePagingState = SimplePagingState.IDLE, + pokitPagingState: PagingState = PagingState.IDLE, loadNextPokits: () -> Unit = {}, refreshPokits: () -> Unit = {}, show: Boolean = false, diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt index 64e927d0..6bc89d98 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.atom.button.PokitButton import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize import pokitmons.pokit.core.ui.components.atom.checkbox.PokitCheckbox @@ -48,7 +49,6 @@ import pokitmons.pokit.search.model.Filter import pokitmons.pokit.search.model.FilterType import pokitmons.pokit.search.model.Pokit import pokitmons.pokit.search.model.samplePokits -import pokitmons.pokit.search.paging.SimplePagingState import pokitmons.pokit.core.ui.R.string as coreString @OptIn(ExperimentalFoundationApi::class) @@ -58,7 +58,7 @@ fun FilterBottomSheetContent( firstShowType: FilterType = FilterType.Pokit, onSaveClilck: (Filter) -> Unit = {}, pokits: List = samplePokits, - pokitPagingState: SimplePagingState = SimplePagingState.IDLE, + pokitPagingState: PagingState = PagingState.IDLE, loadNextPokits: () -> Unit = {}, ) { var currentFilter by remember { mutableStateOf(filter) } @@ -80,7 +80,7 @@ fun FilterBottomSheetContent( } LaunchedEffect(startLinkPaging.value) { - if (startLinkPaging.value && pokitPagingState == SimplePagingState.IDLE) { + if (startLinkPaging.value && pokitPagingState == PagingState.IDLE) { loadNextPokits() } } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt index 75ec7004..95fe34e3 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt @@ -22,10 +22,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.feature.model.paging.PagingState import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.search.model.Link -import pokitmons.pokit.search.paging.SimplePagingState import pokitmons.pokit.core.ui.R.drawable as coreDrawable import pokitmons.pokit.search.R.string as SearchString @@ -35,7 +35,7 @@ internal fun SearchItemList( onToggleSort: () -> Unit = {}, useRecentOrder: Boolean = true, links: List = emptyList(), - linkPagingState: SimplePagingState = SimplePagingState.IDLE, + linkPagingState: PagingState = PagingState.IDLE, onClickLinkKebab: (Link) -> Unit = {}, onClickLink: (Link) -> Unit = {}, loadNextLinks: () -> Unit = {}, @@ -50,7 +50,7 @@ internal fun SearchItemList( } LaunchedEffect(startLinkPaging.value) { - if (startLinkPaging.value && linkPagingState == SimplePagingState.IDLE) { + if (startLinkPaging.value && linkPagingState == PagingState.IDLE) { loadNextLinks() } } @@ -85,7 +85,7 @@ internal fun SearchItemList( LazyColumn( state = linkLazyColumnListState ) { - items(links) { link -> + items(items = links, key = { link -> link.id }) { link -> LinkCard( item = link, title = link.title, diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt deleted file mode 100644 index 54e31ec0..00000000 --- a/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt +++ /dev/null @@ -1,160 +0,0 @@ -package pokitmons.pokit.search.paging - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.usecase.link.SearchLinksUseCase -import pokitmons.pokit.search.model.Filter -import pokitmons.pokit.search.model.Link -import kotlin.coroutines.cancellation.CancellationException - -class LinkPaging( - private val searchLinksUseCase: SearchLinksUseCase, - private val perPage: Int = 10, - private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), - private val initPage: Int = 0, - private val firstRequestPage: Int = 3, - private var filter: Filter, - private var searchWord: String = "", - private var recentSort: Boolean = true, -) : SimplePaging { - - private val _pagingState: MutableStateFlow = MutableStateFlow(SimplePagingState.IDLE) - override val pagingState: StateFlow = _pagingState.asStateFlow() - - private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) - override val pagingData: StateFlow> = _pagingData.asStateFlow() - - private var currentPageIndex = initPage - private var requestJob: Job? = null - - fun changeFilter(filter: Filter) { - this.filter = filter - } - - fun changeSearchWord(searchWord: String) { - this.searchWord = searchWord - } - - fun changeRecentSort(recentSort: Boolean) { - this.recentSort = recentSort - } - - override suspend fun refresh() { - requestJob?.cancel() - - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.LOADING_INIT } - requestJob = coroutineScope.launch { - try { - currentPageIndex = initPage - val response = searchLinksUseCase.searchLinks( - page = currentPageIndex, - size = perPage * firstRequestPage, - sort = listOf(), - isRead = if (filter.notRead) false else null, - favorites = if (filter.bookmark) true else null, - startDate = filter.startDate?.toDateString(), - endDate = filter.endDate?.toDateString(), - categoryIds = filter.selectedPokits.mapNotNull { it.id.toIntOrNull() }, - searchWord = searchWord - ) - when (response) { - is PokitResult.Success -> { - val links = response.result.map { domainLink -> - Link.fromDomainLink(domainLink) - } - applyResponse(links, firstRequestPage) - } - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } - } - - override suspend fun load() { - if (pagingState.value != SimplePagingState.IDLE) return - - requestJob?.cancel() - _pagingState.update { SimplePagingState.LOADING_NEXT } - - requestJob = coroutineScope.launch { - try { - val response = searchLinksUseCase.searchLinks( - page = currentPageIndex, - size = perPage, - sort = listOf(), - isRead = if (filter.notRead) false else null, - favorites = if (filter.bookmark) true else null, - startDate = filter.startDate?.toDateString(), - endDate = filter.endDate?.toDateString(), - categoryIds = filter.selectedPokits.mapNotNull { it.id.toIntOrNull() }, - searchWord = searchWord - ) - when (response) { - is PokitResult.Success -> { - val links = response.result.map { domainLink -> - Link.fromDomainLink(domainLink) - } - applyResponse(links) - } - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } - } - - private fun applyResponse(dataInResponse: List, multiple: Int = 1) { - if (dataInResponse.size < perPage * multiple) { - _pagingState.update { SimplePagingState.LAST } - } else { - _pagingState.update { SimplePagingState.IDLE } - } - _pagingData.update { _pagingData.value + dataInResponse } - currentPageIndex += multiple - } - - override fun clear() { - requestJob?.cancel() - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.IDLE } - } - - override suspend fun deleteItem(item: Link) { - val capturedDataList = _pagingData.value - _pagingData.update { capturedDataList.filter { it.id != item.id } } - } - - override suspend fun modifyItem(item: Link) { - val capturedDataList = _pagingData.value - val targetPokit = capturedDataList.find { it.id == item.id } ?: return - - _pagingData.update { - capturedDataList.map { pokit -> - if (targetPokit.id == pokit.id) { - item - } else { - pokit - } - } - } - } -} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/PokitPaging.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/PokitPaging.kt deleted file mode 100644 index b3a6ae54..00000000 --- a/feature/search/src/main/java/pokitmons/pokit/search/paging/PokitPaging.kt +++ /dev/null @@ -1,124 +0,0 @@ -package pokitmons.pokit.search.paging - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import pokitmons.pokit.domain.commom.PokitResult -import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase -import pokitmons.pokit.search.model.Pokit -import kotlin.coroutines.cancellation.CancellationException - -class PokitPaging( - private val getPokits: GetPokitsUseCase, - private val perPage: Int = 10, - private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), - private val initPage: Int = 0, - private val firstRequestPage: Int = 3, -) : SimplePaging { - private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) - override val pagingState: StateFlow = _pagingState.asStateFlow() - - private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) - override val pagingData: StateFlow> = _pagingData.asStateFlow() - private var currentPageIndex = initPage - private var requestJob: Job? = null - - override suspend fun refresh() { - requestJob?.cancel() - - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.LOADING_INIT } - requestJob = coroutineScope.launch { - try { - currentPageIndex = initPage - val response = getPokits.getPokits(size = perPage * firstRequestPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList, firstRequestPage) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_INIT } - } - } - } - } - - override suspend fun load() { - if (pagingState.value != SimplePagingState.IDLE) return - - requestJob?.cancel() - _pagingState.update { SimplePagingState.LOADING_NEXT } - - requestJob = coroutineScope.launch { - try { - val response = getPokits.getPokits(size = perPage, page = currentPageIndex) - when (response) { - is PokitResult.Success -> { - val pokitList = response.result.map { domainPokit -> - Pokit.fromDomainPokit(domainPokit) - } - applyResponse(pokitList) - } - - is PokitResult.Error -> { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } catch (exception: Exception) { - if (exception !is CancellationException) { - _pagingState.update { SimplePagingState.FAILURE_NEXT } - } - } - } - } - - private fun applyResponse(dataInResponse: List, multiple: Int = 1) { - if (dataInResponse.size < perPage * multiple) { - _pagingState.update { SimplePagingState.LAST } - } else { - _pagingState.update { SimplePagingState.IDLE } - } - _pagingData.update { _pagingData.value + dataInResponse } - currentPageIndex += multiple - } - - override fun clear() { - requestJob?.cancel() - _pagingData.update { emptyList() } - _pagingState.update { SimplePagingState.IDLE } - } - - override suspend fun deleteItem(item: Pokit) { - val capturedDataList = _pagingData.value - _pagingData.update { capturedDataList.filter { it.id != item.id } } - } - - override suspend fun modifyItem(item: Pokit) { - val capturedDataList = _pagingData.value - val targetPokit = capturedDataList.find { it.id == item.id } ?: return - - _pagingData.update { - capturedDataList.map { pokit -> - if (targetPokit.id == pokit.id) { - item - } else { - pokit - } - } - } - } -} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePaging.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePaging.kt deleted file mode 100644 index b80d630c..00000000 --- a/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePaging.kt +++ /dev/null @@ -1,13 +0,0 @@ -package pokitmons.pokit.search.paging - -import kotlinx.coroutines.flow.Flow - -interface SimplePaging { - val pagingData: Flow> - suspend fun refresh() - suspend fun load() - val pagingState: Flow - suspend fun modifyItem(item: T) - suspend fun deleteItem(item: T) - fun clear() -} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePagingState.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePagingState.kt deleted file mode 100644 index 50a931f1..00000000 --- a/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePagingState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package pokitmons.pokit.search.paging - -enum class SimplePagingState { - IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST -}