Skip to content

Instantly share code, notes, and snippets.

@RomanPozdeev
Created February 25, 2020 07:50
Show Gist options
  • Save RomanPozdeev/7019e38fbd41bbd9f157d10f91149869 to your computer and use it in GitHub Desktop.
Save RomanPozdeev/7019e38fbd41bbd9f157d10f91149869 to your computer and use it in GitHub Desktop.
package mtsit.com.finservice.viewbindingdsl
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.hannesdorfmann.adapterdelegates4.AbsListItemAdapterDelegate
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
/**
* Simple DSL builder to create an [AdapterDelegate] that is backed by a [List] as dataset.
* This DSL builds on top of [ViewBinding] so that no findViewById is needed anymore.
*
* @param viewBinding ViewBinding for this adapter delegate.
* @param on The check that should be run if the AdapterDelegate is for the corresponding Item in the datasource.
* In other words its the implementation of [AdapterDelegate.isForViewType].
* @param block The DSL block. Specify here what to do when the ViewHolder gets created. Think of it as some kind of
* initializer block. For example, you would setup a click listener on a Ui widget in that block followed by specifying
* what to do once the ViewHolder binds to the data by specifying a bind block for
* @since 4.1.0
*/
inline fun <reified I : T, T, V : ViewBinding> adapterDelegateViewBinding(
noinline viewBinding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V,
noinline on: (item: T, items: List<T>, position: Int) -> Boolean = { item, _, _ -> item is I },
noinline layoutInflater: (parent: ViewGroup) -> LayoutInflater = { parent ->
LayoutInflater.from(
parent.context)
},
noinline block: AdapterDelegateViewBindingViewHolder<I, V>.() -> Unit
): AdapterDelegate<List<T>> {
return DslViewBindingListAdapterDelegate(
binding = viewBinding,
on = on,
initializerBlock = block,
layoutInflater = layoutInflater)
}
@PublishedApi
internal class DslViewBindingListAdapterDelegate<I : T, T, V : ViewBinding>(
private val binding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V,
private val on: (item: T, items: List<T>, position: Int) -> Boolean,
private val initializerBlock: AdapterDelegateViewBindingViewHolder<I, V>.() -> Unit,
private val layoutInflater: (parent: ViewGroup) -> LayoutInflater
) : AbsListItemAdapterDelegate<I, T, AdapterDelegateViewBindingViewHolder<I, V>>() {
override fun isForViewType(item: T, items: List<T>, position: Int): Boolean = on(
item, items, position
)
override fun onCreateViewHolder(parent: ViewGroup): AdapterDelegateViewBindingViewHolder<I, V> {
val binding = binding(layoutInflater(parent), parent)
return AdapterDelegateViewBindingViewHolder<I, V>(
binding
).also {
initializerBlock(it)
}
}
override fun onBindViewHolder(
item: I,
holder: AdapterDelegateViewBindingViewHolder<I, V>,
payloads: MutableList<Any>
) {
holder._item = item as Any
holder._bind?.invoke(payloads) // It's ok to have an AdapterDelegate without binding block (i.e. static content)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
@Suppress("UNCHECKED_CAST")
val vh = (holder as AdapterDelegateViewBindingViewHolder<I, V>)
vh._onViewRecycled?.invoke()
}
override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean {
@Suppress("UNCHECKED_CAST")
val vh = (holder as AdapterDelegateViewBindingViewHolder<I, V>)
val block = vh._onFailedToRecycleView
return if (block == null) {
super.onFailedToRecycleView(holder)
} else {
block()
}
}
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
@Suppress("UNCHECKED_CAST")
val vh = (holder as AdapterDelegateViewBindingViewHolder<I, V>)
vh._onViewAttachedToWindow?.invoke()
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
@Suppress("UNCHECKED_CAST")
val vh = (holder as AdapterDelegateViewBindingViewHolder<I, V>)
vh._onViewDetachedFromWindow?.invoke()
}
}
/**
* ViewHolder that is used internally if you use [adapterDelegateViewBinding] DSL to create your AdapterDelegate
*
* @since 4.1.0
*/
class AdapterDelegateViewBindingViewHolder<T, V : ViewBinding>(
val binding: V, view: View = binding.root
) : RecyclerView.ViewHolder(view) {
private object Uninitialized
/**
* Used only internally to set the item.
* The outside consumer should always access [item].
* We do that to trick the user for his own convenience since the Item is only available later and is actually var
* (not val) but we rely on mechanisms from RecyclerView and assume that only the main thread can access and set
* this field (as the user scrolls) so that we make [item] look like a val.
*/
internal var _item: Any = Uninitialized
/**
* Get the current bound item.
*/
val item: T
get() = if (_item === Uninitialized) {
throw IllegalArgumentException(
"Item has not been set yet. That is an internal issue. " +
"Please report at https://github.com/sockeqwe/AdapterDelegates"
)
} else {
@Suppress("UNCHECKED_CAST")
_item as T
}
/**
* Get the context.
*
* @since 4.1.1
*/
val context: Context = view.context
/**
* Returns a localized string from the application's package's
* default string table.
*
* @param resId Resource id for the string
* @return The string data associated with the resource, stripped of styled
* text information.
*
* @since 4.1.1
*/
fun getString(@StringRes resId: Int): String {
return context.getString(resId)
}
/**
* Returns a localized formatted string from the application's package's
* default string table, substituting the format arguments as defined in
* [java.util.Formatter] and [java.lang.String.format].
*
* @param resId Resource id for the format string
* @param formatArgs The format arguments that will be used for
* substitution.
* @return The string data associated with the resource, formatted and
* stripped of styled text information.
*
* @since 4.1.1
*/
fun getString(@StringRes resId: Int, vararg formatArgs: Any): String {
return context.getString(resId, *formatArgs)
}
/**
* Returns a color associated with a particular resource ID and styled for
* the current theme.
*
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
* @return A single color value in the form 0xAARRGGBB.
* @throws android.content.res.Resources.NotFoundException if the given ID
* does not exist.
*
* @since 4.1.1
*/
@ColorInt
fun getColor(@ColorRes id: Int): Int {
return ContextCompat.getColor(context, id)
}
/**
* Returns a drawable object associated with a particular resource ID and
* styled for the current theme.
*
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
* @return An object that can be used to draw this resource.
* @throws android.content.res.Resources.NotFoundException if the given ID
* does not exist.
*
* @since 4.1.1
*/
fun getDrawable(@DrawableRes id: Int): Drawable? {
return ContextCompat.getDrawable(context, id)
}
/**
* Returns a color state list associated with a particular resource ID and
* styled for the current theme.
*
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
* @return A color state list.
* @throws android.content.res.Resources.NotFoundException if the given ID
* does not exist.
*/
fun getColorStateList(@ColorRes id: Int): ColorStateList? {
return ContextCompat.getColorStateList(context, id)
}
/**
* This should never be called directly.
* Use [bind] instead which internally sets this field.
*/
internal var _bind: ((payloads: List<Any>) -> Unit)? = null
private set
/**
* This should never be called directly (only called internally)
* Use [onViewRecycled] instead
*/
internal var _onViewRecycled: (() -> Unit)? = null
private set
/**
* This should never be called directly (only called internally)
* Use [onFailedToRecycleView] instead.
*/
internal var _onFailedToRecycleView: (() -> Boolean)? = null
private set
/**
* This should never be called directly (only called internally)
* Use [onViewAttachedToWindow] instead.
*/
internal var _onViewAttachedToWindow: (() -> Unit)? = null
private set
/**
* This should never be called directly (only called internally)
* Use [onViewDetachedFromWindow] instead.
*/
internal var _onViewDetachedFromWindow: (() -> Unit)? = null
private set
/**
* Define here the block that should be run whenever the viewholder get binded.
* You can access the current bound item with [item]. In case you need the position of the bound item inside the
* adapters dataset use [getAdapterPosition].
*/
fun bind(bindingBlock: (payloads: List<Any>) -> Unit) {
if (_bind != null) {
throw IllegalStateException("bind { ... } is already defined. Only one bind { ... } is allowed.")
}
_bind = bindingBlock
}
/**
* @see AdapterDelegate.onViewRecycled
*/
fun onViewRecycled(block: () -> Unit) {
if (_onViewRecycled != null) {
throw IllegalStateException(
"onViewRecycled { ... } is already defined. " +
"Only one onViewRecycled { ... } is allowed."
)
}
_onViewRecycled = block
}
/**
* @see AdapterDelegate.onFailedToRecycleView
*/
fun onFailedToRecycleView(block: () -> Boolean) {
if (_onFailedToRecycleView != null) {
throw IllegalStateException(
"onFailedToRecycleView { ... } is already defined. " +
"Only one onFailedToRecycleView { ... } is allowed."
)
}
_onFailedToRecycleView = block
}
/**
* @see AdapterDelegate.onViewAttachedToWindow
*/
fun onViewAttachedToWindow(block: () -> Unit) {
if (_onViewAttachedToWindow != null) {
throw IllegalStateException(
"onViewAttachedToWindow { ... } is already defined. " +
"Only one onViewAttachedToWindow { ... } is allowed."
)
}
_onViewAttachedToWindow = block
}
/**
* @see AdapterDelegate.onViewDetachedFromWindow
*/
fun onViewDetachedFromWindow(block: () -> Unit) {
if (_onViewDetachedFromWindow != null) {
throw IllegalStateException(
"onViewDetachedFromWindow { ... } is already defined. " +
"Only one onViewDetachedFromWindow { ... } is allowed."
)
}
_onViewDetachedFromWindow = block
}
/**
* Convenience method find a given view with the given id inside the layout
*/
fun <V : View> findViewById(@IdRes id: Int): V = itemView.findViewById(id) as V
}
@RomanPozdeev
Copy link
Author

RomanPozdeev commented Feb 25, 2020

fun cat2AdapterDelegate() = adapterDelegateViewBinding<Cat, DisplayableItem, ItemCatBinding>(
    viewBinding = { layoutInflater, root ->
        ItemCatBinding.inflate(layoutInflater, root, false)
    }, block = {
        binding.name.setOnClickListener {
            Log.d("Click", "Click on $item")
        }
        bind {
            binding.name.text = item.name
        }
    }
)

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