-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* [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 적용
- Loading branch information
Showing
44 changed files
with
590 additions
and
1,281 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingLoadResult.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package pokitmons.pokit.core.feature.model.paging | ||
|
||
import pokitmons.pokit.domain.commom.PokitResult | ||
|
||
sealed interface PagingLoadResult<out T> { | ||
data class Success<T>(val result: List<T>) : PagingLoadResult<T> | ||
data class Error<T>(val errorCode: String) : PagingLoadResult<T> | ||
|
||
companion object { | ||
fun<T, K> fromPokitResult(pokitResult: PokitResult<K>, mapper: (K) -> List<T>): PagingLoadResult<T> { | ||
return if (pokitResult is PokitResult.Success) { | ||
Success(result = mapper(pokitResult.result)) | ||
} else { | ||
Error(errorCode = (pokitResult as PokitResult.Error).error.code) | ||
} | ||
} | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package pokitmons.pokit.core.feature.model.paging | ||
|
||
interface PagingSource<T> { | ||
suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult<T> | ||
} |
5 changes: 5 additions & 0 deletions
5
core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/PagingState.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package pokitmons.pokit.core.feature.model.paging | ||
|
||
enum class PagingState { | ||
IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST | ||
} |
118 changes: 118 additions & 0 deletions
118
core/feature/src/main/java/pokitmons/pokit/core/feature/model/paging/SimplePaging.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ITEM, KEY> ( | ||
private val pagingSource: PagingSource<ITEM>, | ||
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<List<ITEM>> = MutableStateFlow(emptyList()) | ||
val pagingData: StateFlow<List<ITEM>> = _pagingData.asStateFlow() | ||
|
||
private val _pagingState: MutableStateFlow<PagingState> = MutableStateFlow(PagingState.IDLE) | ||
val pagingState: StateFlow<PagingState> = _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<ITEM>, 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 | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 0 additions & 16 deletions
16
core/feature/src/test/java/pokitmons/pokit/core/feature/ExampleUnitTest.kt
This file was deleted.
Oops, something went wrong.
86 changes: 86 additions & 0 deletions
86
core/feature/src/test/java/pokitmons/pokit/core/feature/SimplePagingUnitTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
} | ||
}) |
27 changes: 27 additions & 0 deletions
27
core/feature/src/test/java/pokitmons/pokit/core/feature/model/TestPagingSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> { | ||
override suspend fun load(pageIndex: Int, pageSize: Int): PagingLoadResult<String> { | ||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.