Current scope: simple_xml| all classes
|
kr.open.library.simple_ui.xml.ui.adapter.normal.headerfooter
Coverage Summary for Class: HeaderFooterRcvAdapter (kr.open.library.simple_ui.xml.ui.adapter.normal.headerfooter)
| Class | Method, % | Branch, % | Line, % | Instruction, % |
|---|---|---|---|---|
| HeaderFooterRcvAdapter | 41.5% (17/41) | 47.2% (34/72) | 41.6% (72/173) | 43.2% (340/787) |
| HeaderFooterRcvAdapter$WhenMappings | ||||
| Total | 41.5% (17/41) | 47.2% (34/72) | 41.6% (72/173) | 43.2% (340/787) |
package kr.open.library.simple_ui.xml.ui.adapter.normal.headerfooter
import androidx.annotation.MainThread
import androidx.recyclerview.widget.RecyclerView
import kr.open.library.simple_ui.core.logcat.Logx
import kr.open.library.simple_ui.xml.ui.adapter.normal.base.BaseRcvAdapter
import kr.open.library.simple_ui.xml.ui.adapter.normal.result.NormalAdapterResult
import kr.open.library.simple_ui.xml.ui.adapter.normal.result.toNormalAdapterResult
/**
* RecyclerView.Adapter with header, content, and footer section support.<br><br>
* header, content, footer 섹션을 지원하는 RecyclerView.Adapter입니다.<br>
*
* Features:<br>
* 주요 기능:<br>
* - Header and footer CRUD with precise notify calls.<br>
* - header와 footer CRUD를 정확한 notify 호출과 함께 처리합니다.<br>
* - Resolve adapter position into section type and section position.<br>
* - adapter 위치를 섹션 타입과 섹션 위치로 해석합니다.<br>
* - Map click callbacks from adapter position to content position.<br>
* - 클릭 콜백을 adapter 위치에서 content 위치로 매핑합니다.<br>
* - Per-section bind hooks for header, content, and footer.<br>
* - header, content, footer별 바인딩 훅을 제공합니다.<br>
* - Per-section view type hooks for header, content, and footer.<br><br>
* - header, content, footer별 viewType 훅을 제공합니다.<br>
*
* Data store:<br>
* 데이터 저장소:<br>
* - Backed by [HeaderFooterAdapterData].<br><br>
* - [HeaderFooterAdapterData]가 section 데이터를 관리합니다.<br>
*
* @param ITEM Item type shared across all sections.<br><br>
* 모든 섹션에서 공통으로 사용하는 아이템 타입입니다.<br>
* @param VH ViewHolder type used by this adapter.<br><br>
* 이 어댑터가 사용하는 ViewHolder 타입입니다.<br>
*/
public abstract class HeaderFooterRcvAdapter<ITEM : Any, VH : RecyclerView.ViewHolder> : BaseRcvAdapter<ITEM, VH>() {
internal override val adapterData: HeaderFooterAdapterData<ITEM> = HeaderFooterAdapterData()
/**
* Returns immutable snapshot of current header items.<br><br>
* 현재 header 아이템의 불변 스냅샷을 반환합니다.<br>
*/
@MainThread
public fun getHeaderItems(): List<ITEM> =
runOnMainThread("HeaderFooterRcvAdapter.getHeaderItems") { adapterData.headerItems.toList() }
/**
* Returns immutable snapshot of current footer items.<br><br>
* 현재 footer 아이템의 불변 스냅샷을 반환합니다.<br>
*/
@MainThread
public fun getFooterItems(): List<ITEM> =
runOnMainThread("HeaderFooterRcvAdapter.getFooterItems") { adapterData.footerItems.toList() }
/**
* Returns immutable snapshot of all sections combined.<br><br>
* header + content + footer 전체 섹션의 불변 스냅샷을 반환합니다.<br>
*/
@MainThread
public fun getAllItems(): List<ITEM> = runOnMainThread("HeaderFooterRcvAdapter.getAllItems") {
buildList(getItemCount()) {
addAll(adapterData.headerItems)
addAll(adapterData.contentItems)
addAll(adapterData.footerItems)
}
}
/**
* Replaces all header items immediately.<br><br>
* 전체 header 아이템을 즉시 교체합니다.<br>
* Always reports [NormalAdapterResult.Applied].<br><br>
* 항상 [NormalAdapterResult.Applied]를 전달합니다.<br>
*/
@MainThread
public fun setHeaderItems(items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)? = null) {
assertMainThread("HeaderFooterRcvAdapter.setHeaderItems")
val oldHeaders = adapterData.headerItems.toList()
val oldSize = oldHeaders.size
val newSize = items.size
val canRangeChange = oldSize == newSize &&
oldSize > 0 &&
oldHeaders.indices.all { index ->
getHeaderItemViewType(index, oldHeaders[index]) == getHeaderItemViewType(index, items[index])
}
when {
oldSize == 0 && newSize == 0 -> Unit
oldSize == 0 -> {
adapterData.setHeaderItems(items)
notifyItemRangeInserted(0, newSize)
}
newSize == 0 -> {
adapterData.setHeaderItems(emptyList())
notifyItemRangeRemoved(0, oldSize)
}
canRangeChange -> {
adapterData.setHeaderItems(items)
notifyItemRangeChanged(0, newSize)
}
else -> {
adapterData.setHeaderItems(emptyList())
notifyItemRangeRemoved(0, oldSize)
adapterData.setHeaderItems(items)
notifyItemRangeInserted(0, newSize)
}
}
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Replaces all footer items immediately.<br><br>
* 전체 footer 아이템을 즉시 교체합니다.<br>
* Always reports [NormalAdapterResult.Applied].<br><br>
* 항상 [NormalAdapterResult.Applied]를 전달합니다.<br>
*/
@MainThread
public fun setFooterItems(items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)? = null) {
assertMainThread("HeaderFooterRcvAdapter.setFooterItems")
val oldFooters = adapterData.footerItems.toList()
val oldSize = oldFooters.size
val newSize = items.size
val footerStart = adapterData.footerToAdapterPosition(0)
val canRangeChange = oldSize == newSize &&
oldSize > 0 &&
oldFooters.indices.all { index ->
getFooterItemViewType(index, oldFooters[index]) == getFooterItemViewType(index, items[index])
}
when {
oldSize == 0 && newSize == 0 -> Unit
oldSize == 0 -> {
adapterData.setFooterItems(items)
notifyItemRangeInserted(footerStart, newSize)
}
newSize == 0 -> {
adapterData.setFooterItems(emptyList())
notifyItemRangeRemoved(footerStart, oldSize)
}
canRangeChange -> {
adapterData.setFooterItems(items)
notifyItemRangeChanged(footerStart, newSize)
}
else -> {
adapterData.setFooterItems(emptyList())
notifyItemRangeRemoved(footerStart, oldSize)
adapterData.setFooterItems(items)
notifyItemRangeInserted(footerStart, newSize)
}
}
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Appends a single header item.<br><br>
* header 아이템 1개를 추가합니다.<br>
* Always reports [NormalAdapterResult.Applied].<br><br>
* 항상 [NormalAdapterResult.Applied]를 전달합니다.<br>
*/
@MainThread
public fun addHeaderItem(item: ITEM, onResult: ((NormalAdapterResult) -> Unit)? = null) {
assertMainThread("HeaderFooterRcvAdapter.addHeaderItem")
val insertPosition = adapterData.addHeaderItem(item)
notifyItemInserted(insertPosition)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Inserts a header item at a specific position.<br><br>
* 지정한 위치에 header 아이템을 삽입합니다.<br>
*/
@MainThread
public fun addHeaderItemAt(position: Int, item: ITEM, onResult: ((NormalAdapterResult) -> Unit)? = null) {
val failure = commonDataLogic.validateAddItemAt(position, adapterData.headerItems.size)
if (failure != null) {
runResultCallback(failure.toNormalAdapterResult(), onResult)
return
}
val insertPosition = adapterData.addHeaderItemAt(position, item)
notifyItemInserted(insertPosition)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Appends multiple header items.<br><br>
* 여러 header 아이템을 추가합니다.<br>
*/
@MainThread
public fun addHeaderItems(items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)? = null) {
val failure = commonDataLogic.validateAddItems(items)
if (failure != null) {
runResultCallback(failure.toNormalAdapterResult(), onResult)
return
}
val insertStart = adapterData.addHeaderItems(items)
notifyItemRangeInserted(insertStart, items.size)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Clears all header items.<br><br>
* 모든 header 아이템을 제거합니다.<br>
* Always reports [NormalAdapterResult.Applied].<br><br>
* 항상 [NormalAdapterResult.Applied]를 전달합니다.<br>
*/
@MainThread
public fun clearHeaderItems(onResult: ((NormalAdapterResult) -> Unit)? = null) {
assertMainThread("HeaderFooterRcvAdapter.clearHeaderItems")
if (adapterData.headerItems.isEmpty()) {
runResultCallback(NormalAdapterResult.Applied, onResult)
return
}
val removedCount = adapterData.clearHeaderItems()
notifyItemRangeRemoved(0, removedCount)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Appends a single footer item.<br><br>
* footer 아이템 1개를 추가합니다.<br>
* Always reports [NormalAdapterResult.Applied].<br><br>
* 항상 [NormalAdapterResult.Applied]를 전달합니다.<br>
*/
@MainThread
public fun addFooterItem(item: ITEM, onResult: ((NormalAdapterResult) -> Unit)? = null) {
assertMainThread("HeaderFooterRcvAdapter.addFooterItem")
val insertPosition = adapterData.addFooterItem(item)
notifyItemInserted(insertPosition)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Inserts a footer item at a specific footer position.<br><br>
* 지정한 footer 위치에 footer 아이템을 삽입합니다.<br>
*/
@MainThread
public fun addFooterItemAt(position: Int, item: ITEM, onResult: ((NormalAdapterResult) -> Unit)? = null) {
val failure = commonDataLogic.validateAddItemAt(position, adapterData.footerItems.size)
if (failure != null) {
runResultCallback(failure.toNormalAdapterResult(), onResult)
return
}
val insertPosition = adapterData.addFooterItemAt(position, item)
notifyItemInserted(insertPosition)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Appends multiple footer items.<br><br>
* 여러 footer 아이템을 추가합니다.<br>
*/
@MainThread
public fun addFooterItems(items: List<ITEM>, onResult: ((NormalAdapterResult) -> Unit)? = null) {
val failure = commonDataLogic.validateAddItems(items)
if (failure != null) {
runResultCallback(failure.toNormalAdapterResult(), onResult)
return
}
val insertStart = adapterData.addFooterItems(items)
notifyItemRangeInserted(insertStart, items.size)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Clears all footer items.<br><br>
* 모든 footer 아이템을 제거합니다.<br>
* Always reports [NormalAdapterResult.Applied].<br><br>
* 항상 [NormalAdapterResult.Applied]를 전달합니다.<br>
*/
@MainThread
public fun clearFooterItems(onResult: ((NormalAdapterResult) -> Unit)? = null) {
assertMainThread("HeaderFooterRcvAdapter.clearFooterItems")
if (adapterData.footerItems.isEmpty()) {
runResultCallback(NormalAdapterResult.Applied, onResult)
return
}
val removedCount = adapterData.footerItems.size
val start = adapterData.clearFooterItems()
notifyItemRangeRemoved(start, removedCount)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Binds holder without payloads.<br><br>
* payload 없이 holder를 바인딩합니다.<br>
*/
public override fun onBindViewHolder(holder: VH, position: Int) {
val resolved = resolveSectionPosition(position)
if (resolved == null) {
Logx.e("Cannot bind item, adapterPosition is $position, itemCount $itemCount")
return
}
dispatchBindViewHolder(holder, resolved, emptyList())
}
/**
* 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 resolved = resolveSectionPosition(position)
if (resolved == null) {
Logx.e("Cannot bind item with payload, adapterPosition is $position, itemCount $itemCount")
return
}
dispatchBindViewHolder(holder, resolved, payloads)
}
/**
* Dispatches bind calls to the matching section hook.<br><br>
* 해석된 섹션에 맞는 바인딩 훅으로 분기합니다.<br>
*/
private fun dispatchBindViewHolder(holder: VH, resolved: HeaderFooterRcvAdapterSectionPosition, payloads: List<Any>) {
when (resolved.sectionType) {
HeaderFooterRcvAdapterSectionType.HEADER -> {
val item = adapterData.headerItems.getOrNull(resolved.sectionPosition)
if (item == null) {
Logx.e(
"Cannot bind header item, headerPosition=${resolved.sectionPosition}, headerSize=${adapterData.headerItems.size}"
)
return
}
if (payloads.isEmpty()) onBindHeaderViewHolder(holder, item, resolved.sectionPosition)
else onBindHeaderViewHolder(holder, item, resolved.sectionPosition, payloads)
}
HeaderFooterRcvAdapterSectionType.CONTENT -> {
val item = adapterData.contentItems.getOrNull(resolved.sectionPosition)
if (item == null) {
Logx.e(
"Cannot bind content item, contentPosition=${resolved.sectionPosition}, contentSize=${adapterData.contentItems.size}"
)
return
}
if (payloads.isEmpty()) onBindViewHolder(holder, item, resolved.sectionPosition)
else onBindViewHolder(holder, item, resolved.sectionPosition, payloads)
}
HeaderFooterRcvAdapterSectionType.FOOTER -> {
val item = adapterData.footerItems.getOrNull(resolved.sectionPosition)
if (item == null) {
Logx.e(
"Cannot bind footer item, footerPosition=${resolved.sectionPosition}, footerSize=${adapterData.footerItems.size}"
)
return
}
if (payloads.isEmpty()) onBindFooterViewHolder(holder, item, resolved.sectionPosition)
else onBindFooterViewHolder(holder, item, resolved.sectionPosition, payloads)
}
}
}
/**
* Resolves adapter position into section metadata when within bounds.<br><br>
* adapter 위치가 유효 범위이면 섹션 메타데이터로 해석합니다.<br>
*/
private fun resolveSectionPosition(adapterPosition: Int): HeaderFooterRcvAdapterSectionPosition? {
if (!commonDataLogic.isPositionValid(adapterPosition, itemCount)) {
return null
}
return adapterData.resolveSectionPosition(adapterPosition)
}
/**
* Header payload bind hook.<br><br>
* header payload 바인딩 훅입니다.<br>
*/
protected open fun onBindHeaderViewHolder(holder: VH, item: ITEM, position: Int, payloads: List<Any>) {
onBindHeaderViewHolder(holder, item, position)
}
/**
* Footer full bind hook.<br><br>
* footer 전체 바인딩 훅입니다.<br>
* Default delegates to the content bind contract.<br><br>
* 기본 구현은 content 바인딩 계약으로 위임합니다.<br>
*/
protected open fun onBindFooterViewHolder(holder: VH, item: ITEM, position: Int) {
onBindViewHolder(holder, item, position)
}
/**
* Footer payload bind hook.<br><br>
* footer payload 바인딩 훅입니다.<br>
*/
protected open fun onBindFooterViewHolder(holder: VH, item: ITEM, position: Int, payloads: List<Any>) {
onBindFooterViewHolder(holder, item, position)
}
/**
* Header section view type hook.<br><br>
* header 섹션 viewType 훅입니다.<br>
*/
protected open fun getHeaderItemViewType(position: Int, item: ITEM): Int =
getContentItemViewType(position, item)
/**
* Footer section view type hook.<br><br>
* footer 섹션 viewType 훅입니다.<br>
*/
protected open fun getFooterItemViewType(position: Int, item: ITEM): Int =
getContentItemViewType(position, item)
/**
* Returns the view type for the given adapter position by section.<br><br>
* 주어진 adapter position을 섹션 기준으로 해석해 viewType을 반환합니다.<br>
*/
public override fun getItemViewType(position: Int): Int {
val resolved = resolveSectionPosition(position)
?: return super.getItemViewType(position)
val item = adapterData.getSectionItemOrNull(resolved) ?: return super.getItemViewType(position)
return when (resolved.sectionType) {
HeaderFooterRcvAdapterSectionType.HEADER -> getHeaderItemViewType(resolved.sectionPosition, item)
HeaderFooterRcvAdapterSectionType.CONTENT -> getContentItemViewType(resolved.sectionPosition, item)
HeaderFooterRcvAdapterSectionType.FOOTER -> getFooterItemViewType(resolved.sectionPosition, item)
}
}
/**
* Attaches click and long-click listeners once and maps adapter index to content index.<br><br>
* 클릭 및 롱클릭 리스너를 1회 연결하고 adapter 인덱스를 content 인덱스로 매핑합니다.<br>
*/
final override fun bindClickListeners(holder: VH) {
clickData.attachClickListeners(
holder = holder,
positionMapper = { adapterPosition -> adapterData.adapterToContentPosition(adapterPosition) },
itemProvider = { contentPosition -> getItemOrNull(contentPosition) },
)
clickData.attachLongClickListeners(
holder = holder,
positionMapper = { adapterPosition -> adapterData.adapterToContentPosition(adapterPosition) },
itemProvider = { contentPosition -> getItemOrNull(contentPosition) },
)
}
/**
* Clears all content items immediately. Always returns true.<br><br>
* 모든 content 아이템을 즉시 제거합니다. 항상 true를 반환합니다.<br>
* Always reports [NormalAdapterResult.Applied].<br><br>
* 항상 [NormalAdapterResult.Applied]를 전달합니다.<br>
*/
@MainThread
public override fun removeAll(onResult: ((NormalAdapterResult) -> Unit)?) {
assertMainThread("HeaderFooterRcvAdapter.removeAll")
if (adapterData.contentItems.isEmpty()) {
runResultCallback(NormalAdapterResult.Applied, onResult)
return
}
val removedCount = adapterData.removeAllContentItems()
notifyItemRangeRemoved(adapterData.headerItems.size, removedCount)
runResultCallback(NormalAdapterResult.Applied, onResult)
}
/**
* Header full bind hook.<br><br>
* header 전체 바인딩 훅입니다.<br>
* Default delegates to the content bind contract.<br><br>
* 기본 구현은 content 바인딩 계약으로 위임합니다.<br>
*/
protected open fun onBindHeaderViewHolder(holder: VH, item: ITEM, position: Int) {
onBindViewHolder(holder, item, position)
}
}