Coverage Summary for Class: BaseRcvAdapter (kr.open.library.simple_ui.xml.ui.adapter.normal.base)

Class Class, % Method, % Branch, % Line, % Instruction, %
BaseRcvAdapter 100% (1/1) 95.8% (23/24) 83.3% (25/30) 87.4% (90/103) 85.3% (435/510)


 package kr.open.library.simple_ui.xml.ui.adapter.normal.base
 
 import android.annotation.SuppressLint
 import android.view.View
 import androidx.annotation.MainThread
 import androidx.recyclerview.widget.RecyclerView
 import kr.open.library.simple_ui.core.extensions.trycatch.requireInBounds
 import kr.open.library.simple_ui.core.logcat.Logx
 import kr.open.library.simple_ui.xml.ui.adapter.common.imp.AdapterWriteApi
 import kr.open.library.simple_ui.xml.ui.adapter.normal.headerfooter.HeaderFooterRcvAdapter
 import kr.open.library.simple_ui.xml.ui.adapter.normal.result.NormalAdapterResult
 import kr.open.library.simple_ui.xml.ui.adapter.normal.result.toNormalAdapterResult
 import kr.open.library.simple_ui.xml.ui.adapter.normal.root.RootRcvAdapter
 
 /**
  * Base RecyclerView.Adapter implementation with content item management.<br><br>
  * content 아이템 관리를 제공하는 기본 RecyclerView.Adapter 구현입니다.<br>
  * For header/footer section support, use [HeaderFooterRcvAdapter] instead.<br><br>
  * header/footer 섹션이 필요하면 [HeaderFooterRcvAdapter]를 사용하세요.<br>
  *
  * Features:<br>
  * 주요 기능:<br>
  * - Immediate list mutation with notify-based UI updates.<br>
  * - 리스트를 즉시 변경하고 notify 기반으로 UI를 갱신합니다.<br>
  * - Item click and long-click listener support.<br>
  * - 아이템 클릭 및 롱클릭 리스너를 지원합니다.<br>
  * - Partial bind hook via payloads.<br>
  * - payload 기반 부분 바인딩 훅을 지원합니다.<br>
  * - ViewHolder cache clearing support on recycle.<br><br>
  * - 재활용 시 ViewHolder 캐시 정리를 지원합니다.<br>
  *
  * @param ITEM Item type used by this adapter.<br><br>
  *             이 어댑터가 사용하는 아이템 타입입니다.<br>
  * @param VH ViewHolder type used by this adapter.<br><br>
  *           이 어댑터가 사용하는 ViewHolder 타입입니다.<br>
  */
 public abstract class BaseRcvAdapter<ITEM : Any, VH : RecyclerView.ViewHolder> :
     RootRcvAdapter<ITEM, VH>(),
     AdapterWriteApi<ITEM, NormalAdapterResult> {
     internal open val adapterData: BaseRcvAdapterData<ITEM> = BaseRcvAdapterData()
 
     /**
      * Returns total adapter item count.<br><br>
      * 전체 adapter 아이템 수를 반환합니다.<br>
      */
     public override fun getItemCount(): Int = adapterData.getTotalSize()
 
     /**
      * Returns content item at position or throws when invalid.<br><br>
      * position의 content 아이템을 반환하고 유효하지 않으면 예외를 발생시킵니다.<br>
      */
     @MainThread
     public fun getItem(position: Int): ITEM {
         assertMainThread("BaseRcvAdapter.getItem")
         requireInBounds(isPositionValid(position)) {
             "Invalid content position: $position, contentSize: ${adapterData.contentItems.size}"
         }
         return adapterData.contentItems[position]
     }
 
     /**
      * Returns immutable snapshot of current content items.<br><br>
      * 현재 content 아이템의 불변 스냅샷을 반환합니다.<br>
      */
     @MainThread
     public override fun getItems(): List<ITEM> =
         runOnMainThread("BaseRcvAdapter.getItems") { adapterData.contentItems.toList() }
 
     /**
      * Returns content item at position safely, or null.<br><br>
      * position의 content 아이템을 안전하게 조회하고 없으면 null을 반환합니다.<br>
      */
     @MainThread
     public override fun getItemOrNull(position: Int): ITEM? =
         runOnMainThread("BaseRcvAdapter.getItemOrNull") { adapterData.contentItems.getOrNull(position) }
 
     /**
      * Returns index of target content item, or -1 when not found.<br><br>
      * 대상 content 아이템의 인덱스를 반환하고 없으면 -1을 반환합니다.<br>
      */
     @MainThread
     public override fun getItemPosition(item: ITEM): Int =
         runOnMainThread("BaseRcvAdapter.getItemPosition") { adapterData.contentItems.indexOf(item) }
 
     /**
      * Returns mutable copy of current content items.<br><br>
      * 현재 content 아이템의 가변 복사본을 반환합니다.<br>
      * **Warning**: This is a snapshot copy. Mutations do NOT affect the adapter state.<br><br>
      * 경고: 이 리스트는 스냅샷 복사본이므로 변경해도 adapter 상태에 반영되지 않습니다.<br>
      */
     @MainThread
     public override fun getMutableItemList(): MutableList<ITEM> =
         runOnMainThread("BaseRcvAdapter.getMutableItemList") { adapterData.contentItems.toMutableList() }
 
     /**
      * Replaces all content items immediately.<br><br>
      * 전체 content 아이템을 즉시 교체합니다.<br>
      */
     @SuppressLint("NotifyDataSetChanged")
     @MainThread
     public override fun setItems(items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)?) {
         assertMainThread("BaseRcvAdapter.setItems")
         adapterData.setContentItems(items)
         notifyDataSetChanged()
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Appends a single content item immediately.<br><br>
      * content 아이템 1개를 즉시 추가합니다.<br>
      */
     @MainThread
     public override fun addItem(item: ITEM, onResult: ((NormalAdapterResult) -> Unit)?) {
         assertMainThread("BaseRcvAdapter.addItem")
         val insertPosition = adapterData.addContentItem(item)
         notifyItemInserted(insertPosition)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Inserts a content item at a position immediately.<br><br>
      * 지정한 위치에 content 아이템을 즉시 삽입합니다.<br>
      */
     @MainThread
     public override fun addItemAt(position: Int, item: ITEM, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateAddItemAt(position, adapterData.contentItems.size)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val insertStart = adapterData.addContentItemAt(position, item)
         notifyItemInserted(insertStart)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Appends multiple content items immediately.<br><br>
      * 여러 content 아이템을 즉시 추가합니다.<br>
      */
     @MainThread
     public override fun addItems(items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateAddItems(items)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val insertStart = adapterData.addContentItems(items)
         notifyItemRangeInserted(insertStart, items.size)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Inserts multiple content items at a position immediately.<br><br>
      * 지정한 위치에 여러 content 아이템을 즉시 삽입합니다.<br>
      */
     @MainThread
     public override fun addItemsAt(position: Int, items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateAddItemsAt(items, position, adapterData.contentItems.size)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val insertStart = adapterData.addContentItemsAt(position, items)
         notifyItemRangeInserted(insertStart, items.size)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Removes the first matching content item immediately.<br><br>
      * 첫 번째로 일치하는 content 아이템을 즉시 제거합니다.<br>
      */
     @MainThread
     public override fun removeItem(item: ITEM, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateRemoveItem(item, adapterData.contentItems)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val removePosition = adapterData.removeContentItem(item)
         notifyItemRemoved(removePosition)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Removes matching content items with best-effort semantics.<br><br>
      * best-effort 방식으로 일치하는 content 아이템들을 제거합니다.<br>
      *
      * **Note**: Each removal triggers an individual `notifyItemRemoved` call.<br>
      * For large or contiguous removals, prefer [removeRange] or [removeAll] for better performance.<br><br>
      * **주의**: 각 제거마다 `notifyItemRemoved`가 개별 호출됩니다.<br>
      * 대량 또는 연속 제거는 성능을 위해 [removeRange] 또는 [removeAll]을 사용하세요.<br>
      */
     @MainThread
     public override fun removeItems(items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateRemoveItems(items, adapterData.contentItems)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val removeSet = items.toHashSet()
         val contentIndicesToRemove = adapterData.contentItems
             .mapIndexedNotNull { index, currentItem -> if (currentItem in removeSet) index else null }
             .reversed()
         contentIndicesToRemove.forEach { contentIndex ->
             val adapterPosition = adapterData.contentToAdapterPosition(contentIndex)
             adapterData.removeContentAt(contentIndex)
             notifyItemRemoved(adapterPosition)
         }
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Removes a contiguous content range by start index and count.<br><br>
      * 시작 인덱스와 개수 기준으로 연속된 content 구간을 제거합니다.<br>
      */
     @MainThread
     public override fun removeRange(start: Int, count: Int, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateRemoveRange(start, count, adapterData.contentItems.size)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val startAdapterPosition = adapterData.removeContentRange(start, start + count)
         notifyItemRangeRemoved(startAdapterPosition, count)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Removes content item at position immediately.<br><br>
      * 지정한 위치의 content 아이템을 즉시 제거합니다.<br>
      */
     @MainThread
     public override fun removeAt(position: Int, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateRemoveItemAt(position, adapterData.contentItems.size)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val adapterPosition = adapterData.removeContentAt(position)
         notifyItemRemoved(adapterPosition)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Clears all content items immediately.<br><br>
      * 모든 content 아이템을 즉시 제거합니다.<br>
      */
     @MainThread
     public override fun removeAll(onResult: ((NormalAdapterResult) -> Unit)?) {
         assertMainThread("BaseRcvAdapter.removeAll")
         if (adapterData.contentItems.isEmpty()) {
             runResultCallback(NormalAdapterResult.Applied, onResult)
             return
         }
         val removedCount = adapterData.removeAllContentItems()
         notifyItemRangeRemoved(0, removedCount)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Moves a content item from one position to another immediately.<br><br>
      * content 아이템을 한 위치에서 다른 위치로 즉시 이동합니다.<br>
      */
     @MainThread
     public override fun moveItem(fromPosition: Int, toPosition: Int, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateMoveItem(fromPosition, toPosition, adapterData.contentItems.size)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         if (fromPosition == toPosition) {
             runResultCallback(NormalAdapterResult.Applied, onResult)
             return
         }
         val (fromAdapterPosition, toAdapterPosition) = adapterData.moveContentItem(fromPosition, toPosition)
         notifyItemMoved(fromAdapterPosition, toAdapterPosition)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Replaces content item at position immediately.<br><br>
      * 지정한 위치의 content 아이템을 즉시 교체합니다.<br>
      */
     @MainThread
     public override fun replaceItemAt(position: Int, item: ITEM, onResult: ((NormalAdapterResult) -> Unit)?) {
         val failure = commonDataLogic.validateReplaceItemAt(position, adapterData.contentItems.size)
         if (failure != null) {
             runResultCallback(failure.toNormalAdapterResult(), onResult)
             return
         }
         val adapterPosition = adapterData.replaceContentAt(position, item)
         notifyItemChanged(adapterPosition)
         runResultCallback(NormalAdapterResult.Applied, onResult)
     }
 
     /**
      * Binds holder without payloads.<br><br>
      * payload 없이 holder를 바인딩합니다.<br>
      */
     public override fun onBindViewHolder(holder: VH, position: Int) {
         val item = getItemOrNull(position)
         if (item == null) {
             Logx.e("Cannot bind content item, contentPosition=$position, contentSize=${adapterData.contentItems.size}")
             return
         }
         onBindViewHolder(holder, item, position)
     }
 
     /**
      * Binds holder with payloads when provided.<br><br>
      * payload가 제공되면 holder를 payload 기반으로 바인딩합니다.<br>
      */
     public override fun onBindViewHolder(holder: VH, position: Int, payloads: MutableList<Any>) {
         if (payloads.isEmpty()) {
             onBindViewHolder(holder, position)
             return
         }
 
         val item = getItemOrNull(position)
         if (item == null) {
             Logx.e("Cannot bind content item with payload, contentPosition=$position, contentSize=${adapterData.contentItems.size}")
             return
         }
         onBindViewHolder(holder, item, position, payloads)
     }
 
     /**
      * Checks whether content position is valid in current content bounds.<br><br>
      * 현재 content 범위에서 position 유효성을 확인합니다.<br>
      */
     protected fun isPositionValid(position: Int): Boolean =
         commonDataLogic.isPositionValid(position, adapterData.contentItems.size)
 
     /**
      * Sets item click listener.<br><br>
      * 아이템 클릭 리스너를 설정합니다.<br>
      */
     @MainThread
     public override fun setOnItemClickListener(listener: (Int, ITEM, View) -> Unit) {
         assertMainThread("BaseRcvAdapter.setOnItemClickListener")
         clickData.onItemClickListener = listener
     }
 
     /**
      * Sets item long-click listener.<br><br>
      * 아이템 롱클릭 리스너를 설정합니다.<br>
      */
     @MainThread
     public override fun setOnItemLongClickListener(listener: (Int, ITEM, View) -> Unit) {
         assertMainThread("BaseRcvAdapter.setOnItemLongClickListener")
         clickData.onItemLongClickListener = listener
     }
 }