Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix]#71 feature 공통 기능 리펙토링 및 포킷내 링크 개수 표시관련 수정 #74

Merged
merged 9 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading