Coverage Summary for Class: AdapterOperationQueue (kr.open.library.simple_ui.xml.ui.adapter.list.base.queue)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| AdapterOperationQueue |
91.7%
(11/12)
|
70%
(7/10)
|
90.5%
(38/42)
|
87.3%
(151/173)
|
| AdapterOperationQueue$AddItemAtOp |
100%
(2/2)
|
100%
(4/4)
|
100%
(7/7)
|
100%
(50/50)
|
| AdapterOperationQueue$AddItemOp |
100%
(2/2)
|
|
100%
(4/4)
|
100%
(25/25)
|
| AdapterOperationQueue$AddItemsAtOp |
100%
(2/2)
|
100%
(4/4)
|
100%
(8/8)
|
100%
(58/58)
|
| AdapterOperationQueue$AddItemsOp |
100%
(2/2)
|
|
100%
(5/5)
|
100%
(32/32)
|
| AdapterOperationQueue$ClearItemsOp |
100%
(2/2)
|
|
100%
(3/3)
|
100%
(9/9)
|
| AdapterOperationQueue$MoveItemOp |
100%
(2/2)
|
75%
(6/8)
|
100%
(12/12)
|
98.7%
(78/79)
|
| AdapterOperationQueue$Operation |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(2/2)
|
| AdapterOperationQueue$OperationTerminalState |
|
| AdapterOperationQueue$OperationTerminalState$Applied |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(2/2)
|
| AdapterOperationQueue$OperationTerminalState$Dropped |
100%
(1/1)
|
|
100%
(2/2)
|
100%
(7/7)
|
| AdapterOperationQueue$OperationTerminalState$ExecutionError |
100%
(1/1)
|
|
100%
(2/2)
|
100%
(7/7)
|
| AdapterOperationQueue$RemoveAtOp |
100%
(2/2)
|
75%
(3/4)
|
100%
(6/6)
|
100%
(44/44)
|
| AdapterOperationQueue$RemoveItemOp |
100%
(2/2)
|
100%
(2/2)
|
100%
(6/6)
|
100%
(39/39)
|
| AdapterOperationQueue$RemoveItemsOp |
100%
(2/2)
|
100%
(2/2)
|
100%
(7/7)
|
100%
(38/38)
|
| AdapterOperationQueue$RemoveRangeOp |
100%
(2/2)
|
87.5%
(7/8)
|
100%
(13/13)
|
98.9%
(92/93)
|
| AdapterOperationQueue$ReplaceItemAtOp |
100%
(2/2)
|
100%
(4/4)
|
100%
(7/7)
|
100%
(51/51)
|
| AdapterOperationQueue$SetItemsOp |
100%
(2/2)
|
|
100%
(5/5)
|
100%
(21/21)
|
| Total |
97.5%
(39/40)
|
84.8%
(39/46)
|
96.9%
(127/131)
|
96.7%
(706/730)
|
package kr.open.library.simple_ui.xml.ui.adapter.list.base.queue
import android.os.Handler
import android.os.Looper
import androidx.recyclerview.widget.RecyclerView
import kr.open.library.simple_ui.core.extensions.trycatch.requireInBounds
import kr.open.library.simple_ui.core.logcat.Logx
/**
* Operation queue manager for RecyclerView adapters.<br><br>
* RecyclerView 어댑터 연산 큐 관리 클래스입니다.<br>
*
* Handles sequential execution of list modification operations to prevent race conditions.<br><br>
* 리스트 변경 연산을 순차 실행하여 레이스 컨디션을 방지합니다.<br>
*
* @param ITEM Type of items in the adapter.<br><br>
* 어댑터 아이템 타입입니다.<br>
* @param getCurrentList Function to get current list from adapter.<br><br>
* 어댑터의 현재 리스트를 가져오는 함수입니다.<br>
* @param applyList Function that applies operation result to adapter state/UI.<br><br>
* 연산 결과를 어댑터 상태/UI에 반영하는 함수입니다.<br>
*/
internal class AdapterOperationQueue<ITEM>(
private val getCurrentList: () -> List<ITEM>,
private val applyList: (Operation<ITEM>, List<ITEM>, List<ITEM>, (() -> Unit)?) -> Unit,
) {
/**
* Terminal state delivered to operation callbacks.<br><br>
* 연산 콜백에 전달되는 종료 상태입니다.<br>
*/
internal sealed interface OperationTerminalState {
/**
* Indicates successful application of the operation.<br><br>
* 연산이 성공적으로 반영되었음을 나타냅니다.<br>
*/
data object Applied : OperationTerminalState
/**
* Indicates that the operation was dropped before execution.<br><br>
* 연산이 실행 전에 드롭되었음을 나타냅니다.<br>
*/
data class Dropped(
val reason: QueueDropReason,
) : OperationTerminalState
/**
* Indicates that the operation failed during execution.<br><br>
* 연산이 실행 중 오류로 실패했음을 나타냅니다.<br>
*/
data class ExecutionError(
val cause: Throwable?,
) : OperationTerminalState
}
/**
* Main thread handler for queued operations.<br><br>
* 큐 연산 실행을 위한 메인 스레드 핸들러입니다.<br>
*/
private val mainHandler = Handler(Looper.getMainLooper())
/**
* Shared processor for queued adapter operations.<br><br>
* 어댑터 연산을 공통 엔진으로 처리하는 프로세서입니다.<br>
*/
private val operationProcessor =
OperationQueueProcessor<Operation<ITEM>>(
schedule = { action -> runOnMainThread(action) },
getName = { operation -> operation::class.simpleName ?: "Operation" },
execute = { operation, complete ->
val currentList = getCurrentList()
val updatedList = operation.execute(currentList)
applyList(operation, currentList, updatedList) { complete(QueueExecutionResult.Success) }
},
onComplete = { operation, result ->
when (result) {
is QueueExecutionResult.Success -> {
invokeOperationCallbackSafely(
operation = operation,
state = OperationTerminalState.Applied,
phase = "operation-complete",
)
}
is QueueExecutionResult.Failure -> {
Logx.w("Operation failed: ${operation::class.simpleName}. Callback is invoked as terminal signal.")
invokeOperationCallbackSafely(
operation = operation,
state = OperationTerminalState.ExecutionError(result.cause),
phase = "operation-failed",
)
}
}
},
onDrop = { operation, reason ->
Logx.w(
"Operation dropped: ${operation::class.simpleName}, reason: $reason. Callback is invoked as terminal signal."
)
invokeOperationCallbackSafely(
operation = operation,
state = OperationTerminalState.Dropped(reason),
phase = "operation-dropped",
)
},
onError = { message, error ->
if (error != null) {
Logx.e(message, error)
} else {
Logx.e(message)
}
},
)
/**
* Sealed class representing list operations.<br><br>
* 리스트 연산을 표현하는 Sealed 클래스입니다.<br>
*/
internal sealed class Operation<ITEM> {
abstract val callback: ((OperationTerminalState) -> Unit)?
/**
* Executes operation against current snapshot and returns new snapshot.<br>
* 현재 스냅샷에 연산을 적용해 새 스냅샷을 반환합니다.<br>
*
* Some subclasses re-validate bounds inside [execute] even though the caller already validated
* before enqueuing. This is intentional: between enqueue and execution, other operations may
* have mutated the list, making a previously valid position invalid at execution time.<br>
* If [execute] throws, the queue catches it and delivers [OperationTerminalState.ExecutionError]
* to the callback — not [OperationTerminalState.Dropped] — because the operation had already
* been accepted into the queue and the failure occurred during execution, not at acceptance.<br><br>
* 일부 서브클래스는 호출자가 이미 검증한 후에도 [execute] 내부에서 bounds를 재검증합니다.
* 이는 의도적 설계입니다: 큐 등록 시점과 실행 시점 사이에 다른 연산이 리스트를 변경해
* 이전에는 유효했던 위치가 실행 시점에 무효가 될 수 있기 때문입니다.<br>
* [execute]에서 예외가 발생하면 큐는 이를 잡아 [OperationTerminalState.ExecutionError]로
* 콜백에 전달합니다 — [OperationTerminalState.Dropped]가 아닙니다. 연산이 이미 큐에
* 수락된 후 실행 중에 실패했기 때문입니다.<br>
*/
abstract fun execute(currentList: List<ITEM>): List<ITEM>
}
/**
* Operation that replaces entire list.<br><br>
* 전체 리스트를 교체하는 연산입니다.<br>
*/
internal data class SetItemsOp<ITEM>(
val items: List<ITEM>,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
private val snapshot: List<ITEM> = items.toList()
override fun execute(currentList: List<ITEM>): List<ITEM> = snapshot
}
/**
* Operation that appends one item.<br><br>
* 아이템 1개를 끝에 추가하는 연산입니다.<br>
*/
internal data class AddItemOp<ITEM>(
val item: ITEM,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> = currentList.toMutableList().apply { add(item) }
}
/**
* Operation that inserts one item at position.<br><br>
* 지정 위치에 아이템 1개를 삽입하는 연산입니다.<br>
*/
internal data class AddItemAtOp<ITEM>(
val position: Int,
val item: ITEM,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> {
requireInBounds(position >= 0 && position <= currentList.size) {
"Cannot add item at position $position. Valid range: 0..${currentList.size}"
}
return currentList.toMutableList().apply { add(position, item) }
}
}
/**
* Operation that appends multiple items.<br><br>
* 아이템 여러 개를 끝에 추가하는 연산입니다.<br>
*/
internal data class AddItemsOp<ITEM>(
val items: List<ITEM>,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
private val snapshot: List<ITEM> = items.toList()
override fun execute(currentList: List<ITEM>): List<ITEM> = currentList.toMutableList().apply { addAll(snapshot) }
}
/**
* Operation that inserts multiple items at position.<br><br>
* 지정 위치에 아이템 여러 개를 삽입하는 연산입니다.<br>
*/
internal data class AddItemsAtOp<ITEM>(
val position: Int,
val items: List<ITEM>,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
private val snapshot: List<ITEM> = items.toList()
override fun execute(currentList: List<ITEM>): List<ITEM> {
requireInBounds(position >= 0 && position <= currentList.size) {
"Cannot add items at position $position. Valid range: 0..${currentList.size}"
}
return currentList.toMutableList().apply { addAll(position, snapshot) }
}
}
/**
* Operation that removes one item at position.<br><br>
* 지정 위치의 아이템 1개를 제거하는 연산입니다.<br>
*/
internal data class RemoveAtOp<ITEM>(
val position: Int,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> {
requireInBounds(position >= 0 && position < currentList.size) {
"Cannot remove item at position $position. Valid range: 0 until ${currentList.size}"
}
return currentList.toMutableList().apply { removeAt(position) }
}
}
/**
* Operation that removes first matching item.<br><br>
* 일치하는 첫 번째 아이템을 제거하는 연산입니다.<br>
*/
internal data class RemoveItemOp<ITEM>(
val item: ITEM,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> {
val position = currentList.indexOf(item)
requireInBounds(position != RecyclerView.NO_POSITION) { "Item not found in the list" }
return currentList.toMutableList().apply { removeAt(position) }
}
}
/**
* Operation that removes all matching items (best-effort).<br><br>
* 일치하는 아이템들을 best-effort로 제거하는 연산입니다.<br>
*/
internal data class RemoveItemsOp<ITEM>(
val items: List<ITEM>,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
private val snapshot: List<ITEM> = items.toList()
override fun execute(currentList: List<ITEM>): List<ITEM> {
if (snapshot.isEmpty()) return currentList
val removeSet = snapshot.toHashSet()
return currentList.filter { it !in removeSet }
}
}
/**
* Operation that removes a contiguous range.<br><br>
* 연속된 구간을 제거하는 연산입니다.<br>
*/
internal data class RemoveRangeOp<ITEM>(
val start: Int,
val count: Int,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> {
requireInBounds(count > 0) {
"Cannot remove range, count must be positive. count: $count"
}
requireInBounds(start >= 0 && start < currentList.size) {
"Cannot remove range from start $start. Valid range: 0..${currentList.lastIndex}"
}
requireInBounds(count <= currentList.size - start) {
"Cannot remove range start=$start, count=$count. Valid max count: ${currentList.size - start}"
}
return currentList.toMutableList().apply {
subList(start, start + count).clear()
}
}
}
/**
* Operation that clears all items.<br><br>
* 모든 아이템을 비우는 연산입니다.<br>
*/
internal data class ClearItemsOp<ITEM>(
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> = emptyList()
}
/**
* Operation that moves one item between positions.<br><br>
* 아이템 1개를 위치 간 이동하는 연산입니다.<br>
*/
internal data class MoveItemOp<ITEM>(
val fromPosition: Int,
val toPosition: Int,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> {
requireInBounds(fromPosition >= 0 && fromPosition < currentList.size) {
"Invalid fromPosition $fromPosition. Valid range: 0 until ${currentList.size}"
}
requireInBounds(toPosition >= 0 && toPosition < currentList.size) {
"Invalid toPosition $toPosition. Valid range: 0 until ${currentList.size}"
}
return currentList.toMutableList().apply {
val item = removeAt(fromPosition)
add(toPosition.coerceAtMost(size), item)
}
}
}
/**
* Operation that replaces one item at position.<br><br>
* 지정 위치의 아이템 1개를 교체하는 연산입니다.<br>
*/
internal data class ReplaceItemAtOp<ITEM>(
val position: Int,
val item: ITEM,
override val callback: ((OperationTerminalState) -> Unit)?,
) : Operation<ITEM>() {
override fun execute(currentList: List<ITEM>): List<ITEM> {
requireInBounds(position >= 0 && position < currentList.size) {
"Invalid position $position. Valid range: 0 until ${currentList.size}"
}
return currentList.toMutableList().apply { set(position, item) }
}
}
/**
* Enqueues an operation and processes it.<br><br>
* 연산을 큐에 추가하고 처리합니다.<br>
*/
fun enqueueOperation(operation: Operation<ITEM>) {
operationProcessor.enqueue(operation)
}
/**
* Clears the operation queue and processes the given operation immediately.<br><br>
* 큐를 비우고 전달된 연산을 즉시 처리합니다.<br>
*/
fun clearQueueAndExecute(operation: Operation<ITEM>) {
operationProcessor.clearAndEnqueue(operation)
}
/**
* Updates queue overflow policy and max pending size.<br><br>
* 큐 오버플로 정책과 최대 대기 크기를 설정합니다.<br>
*/
fun setQueuePolicy(maxPending: Int, overflowPolicy: QueueOverflowPolicy) {
operationProcessor.setQueuePolicy(maxPending, overflowPolicy)
}
/**
* Executes the action on the main thread, posting if necessary.<br><br>
* 메인 스레드에서 실행하며 필요 시 메인으로 포스트합니다.<br>
*/
private fun runOnMainThread(action: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
action()
} else {
mainHandler.post(action)
}
}
/**
* Invokes operation callback safely.<br><br>
* 연산 콜백을 안전하게 호출합니다.<br>
*/
private fun invokeOperationCallbackSafely(
operation: Operation<ITEM>,
state: OperationTerminalState,
phase: String,
) {
try {
operation.callback?.invoke(state)
} catch (e: RuntimeException) {
Logx.e("Error in operation callback ($phase)", e)
}
}
}