Skip to content

Commit

Permalink
[Fix]#71 feature 공통 기능 리펙토링 및 포킷내 링크 개수 표시관련 수정 (#74)
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
l5x5l authored Sep 22, 2024
1 parent e79a730 commit a05a8ba
Show file tree
Hide file tree
Showing 44 changed files with 590 additions and 1,281 deletions.
14 changes: 14 additions & 0 deletions core/feature/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ plugins {
}

android {
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

namespace = "pokitmons.pokit.core.feature"
compileSdk = 34

Expand Down Expand Up @@ -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"))
}
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)
}
}
}
}
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>
}
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
}
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
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ data class PokitArg(
val imageUrl: String,
val title: String,
) : Parcelable

data class LinkCountChangedPokitIds(
val increasedPokitId: Int?,
val decreasedPokitId: Int?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ object PokitUpdateEvent {
private val _addedPokit = MutableSharedFlow<PokitArg>()
val addedPokit = _addedPokit.asSharedFlow()

private val _countModifiedPokitIds = MutableSharedFlow<LinkCountChangedPokitIds>()
val countModifiedPokitIds = _countModifiedPokitIds.asSharedFlow()

fun updatePokit(pokitArg: PokitArg) {
CoroutineScope(Dispatchers.Default).launch {
_updatedPokit.emit(pokitArg)
Expand All @@ -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))
}
}
}

This file was deleted.

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
}
}
}
})
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -106,7 +106,7 @@ fun AddLinkScreenContainer(
}

LaunchedEffect(startPaging.value) {
if (startPaging.value && pokitListState == SimplePagingState.IDLE) {
if (startPaging.value && pokitListState == PagingState.IDLE) {
viewModel.loadNextPokits()
}
}
Expand Down
Loading

0 comments on commit a05a8ba

Please sign in to comment.