Yet another adapter delegate library.
repositories {
...
maven { url 'https://jitpack.io' }
}
...
dependencies {
implementation("com.github.Miha-x64:Delegapter:0.98")
}
Why not sockeqwe/AdapterDelegates?
The idea of registering a delegate for a certain item type is flawed:
- one could forget to register a delegate (runtime crash)
- or to unregister a useless one (dead code)
- having items of the same type with different
viewTypesandViewHolders is impossible - having items of different generic types with the same raw type is impossible
The concept of this library is to make everything clear and explicit. No binding a delegate to certain item type, no fallback delegates.
We use our own ViewHolder class (called just VH) for a bunch of reasons:
RecyclerView.ViewHolderis abstract, but it's sometimes necessary to create a “dumb” holder without any special fields or behaviour, thusVHisopen- There's
RecyclerView.ViewHolder.itemView: View, butVHis generic, and has a propertyVH<V, …>.view: V - When using viewBinding, all
ViewHolders look the same: they havebindingfield.VHsupports an attachment of any type which is typicallyViewBinding:VH<*, B, …>.binding: B - Delegapter needs to tie certain
ViewHoldertype with the corresponding data type for type safety:VH<V : View, B, D> - Therefore,
VH<*, *, D>has its ownbind(D)method which is a common practice
There's a lot of factory functions for creating ViewHolders:
VH(TextView(parent.context).apply {
layoutParams = RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
fontRes = R.font.roboto
textSize = 17f
}, TextView::setText) // VH<TextView, Nothing?, CharSequence>
inflateVH(parent, ItemUserBinding::inflate) { user: User ->
imageLoader.load(user.photo).into(photoView)
nameView.text = user.name
} // VH<View, ItemUserBinding, User>
// and more…Delegate is just a ViewHolder factory:
typealias Delegate<D> = (parent: ViewGroup) -> VH<*, *, D>VH::V and VH::B are actually implementation details of a certain VH, Delegapter does not need them after instantiation, thus <*, *.
A typical Delegate declaration looks like this:
val userDelegate = "user" { parent: ViewGroup ->
inflateVH(…) { … }
}The string before lambda makes it go through library's String.invoke(lambda) function to make it named for debugging purposes. (Unfortunately, tagged@ { lambda } has no effect on toString().)
Of course, plain lambdas are accepted, too. And ::function references are also OK and named on their own.
Delegapter is basically a list of (item, delegate) tuples, but their type agreement is guaranteed, like it was a List<<D> Pair<Delegate<D>, D> (non-denotable type in Java/Kotlin).
Delegapter is not an Adapter itself, just a special data structure. Let's use DelegatedAdapter for convenience, it already has val data = Delegapter(this, …) property inside:
class SomeAdapter : DelegatedAdapter() {
init { stateRestorationPolicy = … }
fun update(item: Data) {
data.clear()
data.add(headerDelegate, item.header)
data.addAll(recommendationDelegate, item.recommended)
data.addAll(postDelegate, item.posts)
// use autocomplete to see all available functions
}
}You may want to use Delegapter with a custom adapter in some advanced usage scenarios:
- Insert items not handled by
Delegapter(headers, footers, ads 🤮). (Instead of passingthisto the constructor, use customListUpdateCallbackimplementation to correctnotify*()calls) - Filter out some items without removing them
(this requires a corrected
ListUpdateCallback, too) - Use several Delegapters in a single Adapter (IDK why but this should happen at some point)
In order to share RecycledViewPool between several RecyclerViews, you need to use MutableDelegapter.recycledViewPool and preserve the same viewType to Delegate mapping across adapters. The latter can be achieved using a shared “parent” Delegapter:
val delegapterFather = Delegapter(NullListUpdateCallback)
class SomeAdapter : RecyclerView.Adapter<…>() { // for custom adapter
private val d = Delegapter(this, delegapterFather)
…
}
val otherAdapter = DelegatedAdapter(delegapterFather) // using pre-baked adapterApart from skeletal VHAdapter and ready-to-use DelegatedAdapter, there are two more: RepeatAdapter and SingleTypeAdapter. They don't use Delegapter but employ VH and Delegate for the ease of reuse.
In order to use DiffUtil, you need to call replace { } function on a Delegapter instance:
data.replace {
add(...)
}A temporary instance of Delegapter subclass will be passed to the lambda. Its mutation API is quite similar but requires all your delegates to be DiffDelegate. Apart from implementing this interface directly (which is boring) there are two more ways:
val someDelegate = "some delegate" { … }.diff(
areItemsTheSame = equateBy(SomeItem::id), /*
areContentsTheSame = Any::equals,
getChangePayload = { _, _ -> null },
*/)
val otherDelegate = "otherDelegate" { … } + object : DiffUtil.ItemCallback() {
override fun are...TheSame(...) = ...
}This utility is super simple:
layoutManager = GridLayoutManager(context, spanCount, orientation, false).apply {
spanSizeLookup = delegapter.spanSizeLookup { position, item, delegate ->
if (delegate == wideDelegate) spanCount else 1
}
}Decorating different viewTypes is a stressful job. Here's how Delegapter helps you to add spaces and dividers for items of certain types:
data.decor(RecyclerView.VERTICAL) {
// keep 16dp after header, before user
between({ it === headerDelegate }, { it === userDelegate }, size = 16)
// keep 30dp between any two users
between({ it === userDelegate }, size = 30)
// text units for text items!
between({ it === textDelegate }, size = 16, unit = COMPLEX_UNIT_SP)
// dividers
after({ it === titleDelegate }, size = 1, drawable = ColorDrawable(Color.BLACK))
// dividers with spaces
after(
{ it === titleDelegate },
size = 5,
drawable = GradientDrawable().apply {
setColor(Color.BLACK)
setSize(0, dp(1))
},
drawableGravity = Gravity.CENTER_VERTICAL or Gravity.FILL_HORIZONTAL,
)
}Predicates like { it === headerDelegate } look clumsy but are very flexible because you can check for several conditions there, for example, match any type ({ true }) or check for external conditions ({ useTextSpaces && it === textDelegate }).
Drawable will receive state and alpha from the View it belongs to. bindingAdapterPosition (or -1 - layoutPosition, if the former is not available) will be passed to Drawable as level.
Note: strictly speaking, between() means “attach decoration after previous matching item if next item matches”. Thus, if you're adding new item, you need to notifyItemChanged(previousItemIndex, anyDummy) to make this decoration appear.
One more precaution: .decor() doesn't know which LayoutManager you use. With Grid one, it's your responsibility to mind about rows and columns.
Any tool can make you happy until it works fine. And make you hate your job when something gets screwed up. A virtue of any abstraction level is an ability to peek into and see what actually happens. If you feel sad, just pass some booleans around: decor(orientation, debugDelegates = true, debugSpaces = true). This will show you which delegate is used for each item (that's where having named lambdas helps!), or highlight spaces (as on the screenshot).
