Skip to content

Instantly share code, notes, and snippets.

@rizafu
Last active December 21, 2022 16:00
Show Gist options
  • Save rizafu/bfb2aca68132faa7b948c967d3411058 to your computer and use it in GitHub Desktop.
Save rizafu/bfb2aca68132faa7b948c967d3411058 to your computer and use it in GitHub Desktop.
Android Single Recycler View for multiple view type with ListAdapter and AsyncDifferConfig
interface ItemModel{
/**
* this function for rendering layout id to view holder
*/
@LayoutRes fun layoutId(): Int
/**
* this function for stable id in recyclerView
*/
fun id(): Long = hashCode().toLong()
/**
* this function for checking data item to rendering new data.
* for simple comparison just implement data class and use toString() function
*/
fun contentsTheSame(newItem: ItemModel): Boolean = toString() == newItem.toString()
}
abstract class BaseViewHolder<T: ItemModel>(itemView: View): RecyclerView.ViewHolder(itemView){
internal var onAfterTextChanged: ((view: View, itemModel: ItemModel, text: Editable?)-> Unit)? = null
internal var onItemClick: ((view: View, itemModel: ItemModel)-> Unit)? = null
internal var sharedViewPool: RecyclerView.RecycledViewPool? = null
/**
* call this on init view holder, do not call this function on [bind] to avoid UI lag or memory leak
*/
protected fun View.addOnItemClick(localOnItemClick: ((view: View, item: T)-> T)? = null){
setOnClickListener {
if (itemView.tag is ItemModel){
onItemClick?.invoke(it, localOnItemClick?.invoke(it,getItemModel())?:getItemModel())
?:localOnItemClick?.invoke(it, getItemModel())
}
}
}
/**
* set [View.setTag] on [bind] to make unique tag/key for each editText.
* call this on init view holder, do not call this function on [bind] to avoid UI lag or memory leak
* @param localOnAfterTextChanged implement this for manipulate data on local view holder
*/
protected fun EditText.addOnAfterTextChanged(localOnAfterTextChanged: ((view: View, item: T, text: Editable?) -> Editable?)? = null){
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
this@addOnAfterTextChanged.removeTextChangedListener(this)
if (itemView.tag is ItemModel){
onAfterTextChanged?.invoke(this@addOnAfterTextChanged, getItemModel(),
localOnAfterTextChanged?.invoke(this@addOnAfterTextChanged, getItemModel(), s)?:s
) ?: localOnAfterTextChanged?.invoke(this@addOnAfterTextChanged, getItemModel(), s)
}
this@addOnAfterTextChanged.addTextChangedListener(this)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
/**
* call this on init view holder, do not call this function on [bind] to avoid UI lag or memory leak
*/
protected fun RecyclerViewListAdapter.addOnNestedItemClick(localOnItemClick: ((view: View, item: ItemModel)-> ItemModel)? = null){
setOnItemClick { view, item -> onItemClick?.invoke(view, localOnItemClick?.invoke(view,item)?:item)
?:localOnItemClick?.invoke(view,item)
}
}
/**
* use shared view pool for performance reason to share view holder on nested recyclerView
* call this on init view holder after set [RecyclerView.LayoutManager] base on layout behavior
* do not call this function on [bind] to avoid UI lag or memory leak
* @param maxRecycledViews value of pair is Pair<ViewType, Max>
* @param initialPrefetchItemCount minimum number of view visible per item
*/
protected fun RecyclerView.setSharedViewPool(initialPrefetchItemCount: Int = 5, vararg maxRecycledViews: Pair<Int,Int>){
sharedViewPool?.let {
(layoutManager as LinearLayoutManager).apply {
this.recycleChildrenOnDetach = true
this.initialPrefetchItemCount = initialPrefetchItemCount
}
maxRecycledViews.forEach { pair ->
val (viewType, max) = pair
it.setMaxRecycledViews(viewType, max)
}
setRecycledViewPool(it)
}
}
@Suppress("UNCHECKED_CAST")
fun getItemModel(): T = itemView.tag as T
abstract fun bind(item: T)
}
interface ViewHolderFactory{
@LayoutRes fun layoutId(): Int
fun createViewHolder(viewItem: View): BaseViewHolder<*>
fun bindViewHolder(viewHolder: BaseViewHolder<*>, itemModel: ItemModel)
}
object DiffItemModel: DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean = oldItem.id() == newItem.id()
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean = oldItem.contentsTheSame(newItem)
}
class RecyclerViewListAdapter: ListAdapter<ItemModel, BaseViewHolder<*>>(AsyncDifferConfig.Builder(DiffItemModel).build()) {
init {
setHasStableIds(true)
}
private val factories: MutableList<ViewHolderFactory> = mutableListOf()
private var nestedViewPool: RecyclerView.RecycledViewPool? = null
private var onItemClickListener: ((view: View, item: ItemModel)-> Unit)? = null
private var onAfterTextChanged: ((view: View, item: ItemModel, text: Editable?)-> Unit)? = null
fun registerViewHolderFactory(viewHolderFactory: ViewHolderFactory) = apply {
factories.add(viewHolderFactory)
factories.distinctBy { it.layoutId() }
}
fun setOnItemClick(onItemClick: (view: View, item: ItemModel)-> Unit) = apply{
this.onItemClickListener = onItemClick
}
fun setOnAfterTextChanged(onAfterTextChanged: (view: View, item: ItemModel, text: Editable?)-> Unit) = apply{
this.onAfterTextChanged = onAfterTextChanged
}
fun setNestedViewPool(nestedViewPool: RecyclerView.RecycledViewPool) = apply {
this.nestedViewPool = nestedViewPool
}
override fun getItemId(position: Int): Long = getItem(position).id()
override fun getItemViewType(position: Int): Int = getItem(position).layoutId()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return factories.firstOrNull { it.layoutId() == viewType }
?.createViewHolder(LayoutInflater.from(parent.context).inflate(viewType, parent, false)).also {
it?.onAfterTextChanged = this.onAfterTextChanged
it?.onItemClick = this.onItemClickListener
it?.sharedViewPool = this.nestedViewPool
} ?: throw Throwable("ViewHolder of ${parent.context.resources.getResourceEntryName(viewType)} not registered yet")
}
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
holder.itemView.tag = getItem(position)
factories.firstOrNull { it.layoutId() == getItemViewType(position) }?.bindViewHolder(holder,getItem(position))
}
}
@patrickdip
Copy link

hi guy, in witch case this is really useful?

@rizafu
Copy link
Author

rizafu commented Dec 28, 2020

hi guy, in witch case this is really useful?

hi, you can check out my mini project using this framework. Hope it can be clear
thanks
https://github.com/rizafu/MovieDB

@amit7127
Copy link

hi guy, in witch case this is really useful?

hi, you can check out my mini project using this framework. Hope it can be clear
thanks
https://github.com/rizafu/MovieDB

ViewHolder shouldn't directly access the Repository.

@otoo-peacemaker
Copy link

hi guy, in witch case this is really useful?

hi, you can check out my mini project using this framework. Hope it can be clear thanks https://github.com/rizafu/MovieDB

do a simple project this looks confusing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment