Skip to content

Commit

Permalink
📝 Add documentation for BindingAdapters
Browse files Browse the repository at this point in the history
  • Loading branch information
David Sucharda committed Jun 30, 2021
1 parent 0a0a3cc commit 150eb3e
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntRange
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.Lifecycle
Expand All @@ -19,8 +20,11 @@ import cz.eman.kaal.presentation.adapter.binder.VariableBinder
* Handles basic (common) functionality for Binding adapters. Takes care of view creation, data binding, click listeners
* and other common functionality for adapters.
*
* This is an interface due to different parents for adapters. It accesses the variable by functions and
* This is an interface due to different parents for adapters. It requires access to multiple variables from the
* extending classes like [itemBinder], [itemClickListener], and others. Prevents code being repeated but it also makes
* the variables public.
*
* @property T type of the object handled by this interface
* @author eMan a.s.
* @author Radek Piekarz
* @see [GitHub project](https://github.com/radzio/android-data-binding-recyclerview)
Expand All @@ -31,17 +35,63 @@ import cz.eman.kaal.presentation.adapter.binder.VariableBinder
interface BaseBindingAdapter<T : Any> : View.OnClickListener,
View.OnLongClickListener {

/**
* Gets item on a specific position in the adapter. Must be implemented by extending class.
*
* @param position of the item
* @return [T] item or null when not found
*/
fun getItemInternal(@IntRange(from = 0) position: Int): T?

/**
* Item binder specifies binding layout for specific item. For more information check [ItemBinder].
*/
val itemBinder: ItemBinder<T>

/**
* Specifies click action for the whole item layout ([ViewHolder]). Click on any sub-view of the holder must be
* handled separately. Ex: you can send ViewModel using [variableBinders] and bind click directly in layout xml.
*/
val itemClickListener: ((View, T) -> Unit)?

/**
* Same as the [itemClickListener] with the exception that it handles long click instead of click.
*/
val itemLongClickListener: ((View, T) -> Unit)?

/**
* Used to send custom variables to item layout [ViewHolder]. Any variable can be send but most common is ViewModel.
* For more information check [VariableBinder].
*/
val variableBinders: Array<VariableBinder<T>>?

/**
* Creates instance of [ViewHolder] with specific binding layout inflated using [layoutId].
*
* @param viewGroup for the layout inflation
* @param layoutId layout resource identifying which layout should be inflated
* @return inflated [ViewHolder]
* @see ViewHolder
* @see DataBindingUtil.inflate
*/
fun onCreateViewHolderInternal(viewGroup: ViewGroup, @LayoutRes layoutId: Int): ViewHolder {
return ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.context), layoutId, viewGroup, false))
}

/**
* Binds item data to the [ViewHolder]. Gets the specific item (if possible) and sets it to the view holder binding.
* It also sets any other variables from [variableBinders] binds click listeners (if they are enabled).
*
* Since [ViewHolder] allows variable observing using [Lifecycle] it will set the binding [LifecycleOwner] as a
* [ViewHolder]. It makes sure all variable changes are propagated to the binding. After binding is configured it
* also triggers [ViewDataBinding.executePendingBindings] to make sure all displayed information correspond with the
* item information.
*
* @param viewHolder to have variables and listeners set
* @param position of the item in the list
* @see ViewDataBinding.setVariable
* @see ViewDataBinding.executePendingBindings
*/
fun onBindViewHolderInternal(viewHolder: ViewHolder, position: Int) {
val item = getItemInternal(position) ?: return
viewHolder.binding.apply {
Expand All @@ -63,45 +113,99 @@ interface BaseBindingAdapter<T : Any> : View.OnClickListener,
}
}

fun onViewAttachedToWindowInternal(holder: ViewHolder) {
holder.onStart()
/**
* When view is attached to the window the [ViewHolder] starts observing variables.
*
* @param viewHolder to start listening in
* @see ViewHolder.onStart
*/
fun onViewAttachedToWindowInternal(viewHolder: ViewHolder) {
viewHolder.onStart()
}

fun onViewDetachedFromWindowInternal(holder: ViewHolder) {
holder.onStop()
/**
* When view is detached from the window the [ViewHolder] stops observing variables.
*
* @param viewHolder to start listening in
* @see ViewHolder.onStop
*/
fun onViewDetachedFromWindowInternal(viewHolder: ViewHolder) {
viewHolder.onStop()
}

/**
* Gets item view type by it's [position]. Gets the item using [getItemInternal] and then it gets layout resource
* for it from the [itemBinder]. If it is not found then 0 is returned.
*
* @param position of the item in the list
* @return [Int] with layout resource
* @see getItemInternal
* @see ItemBinder.getLayoutRes
*/
@LayoutRes
fun getItemViewTypeInternal(position: Int) =
getItemInternal(position)?.let { itemBinder.getLayoutRes(it) } ?: 0

override fun onClick(v: View) {
/**
* Handles click on the item view by getting the item from view tag and invoking [itemClickListener] if it exists.
*
* @param view used to get the item and also being send to the click listener
*/
override fun onClick(view: View) {
itemClickListener?.let {
val item = v.getTag(R.id.recycler_view_adapter_item_model) as T
it.invoke(v, item)
val item = view.getTag(R.id.recycler_view_adapter_item_model) as T
it.invoke(view, item)
}
}

override fun onLongClick(v: View): Boolean {
/**
* Handles long click on the item view by getting the item from view tag and invoking [itemLongClickListener] if it
* exists.
*
* @param view used to get the item and also being send to the click listener
* @return true if handled by the [itemLongClickListener] else false
*/
override fun onLongClick(view: View): Boolean {
itemLongClickListener?.let {
val item = v.getTag(R.id.recycler_view_adapter_item_model) as T
it.invoke(v, item)
val item = view.getTag(R.id.recycler_view_adapter_item_model) as T
it.invoke(view, item)
return true
}
return false
}

/**
* View holder for the [RecyclerView.Adapter] holding [ViewDataBinding] for the item and also serving as a
* [LifecycleOwner] to listen for any changes in lifecycle events.
*
* @property binding view binding for the holder
* @see RecyclerView.ViewHolder
*/
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root),
LifecycleOwner {

/**
* @see LifecycleRegistry
*/
private val registry: LifecycleRegistry = LifecycleRegistry(this)

/**
* @see LifecycleOwner.getLifecycle
*/
override fun getLifecycle(): Lifecycle = registry

/**
* Sets [Lifecycle.Event.ON_START] event to the [registry] to start observing variable changes.
*/
@MainThread
fun onStart() {
registry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}

/**
* Sets [Lifecycle.Event.ON_STOP] event to the [registry] to stop observing variable changes.
*/
@MainThread
fun onStop() {
registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import cz.eman.kaal.presentation.adapter.binder.ItemBinder
import cz.eman.kaal.presentation.adapter.binder.VariableBinder

/**
* Wrapper data class for configuration of Binding adapter.
* Wrapper data class for Binding adapter configuration. It is used when binding adapter is being created. Holds only
* common information and all extending information must be handled separately.
*
* @property items displayed in the adapter
* @property itemBinder defines layout for specific item types
* @property variableBinders allows sending custom variables to the layout
* @property itemClickListener enables to handle item click (not sub-view click)
* @property itemLongClickListener enables to handle item long click (not sub-view click)
* @property limit of how many items can be displayed
* @author: eMan a.s.
* @since 0.9.0
*/
data class BindingAdapterConfig<T : Any>(
val items: Collection<T>?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import cz.eman.kaal.presentation.adapter.binder.VariableBinder
import cz.eman.logger.logError

/**
* Binds adapter to [RecyclerView] using [BindingRecyclerViewAdapter]. Parameters are used to create this adapter and
* support item clicks, differences and other options. If the adapter already exists it only changes item list in
* the adapter, which happens with Observable or LiveData items.
* Binds adapter to [RecyclerView] using [BindingRecyclerViewAdapter] or [BindingPagingRecyclerViewAdapter] (based on
* the [paging] variable). Parameters are used to create adapter and support item clicks, differences and other options.
* If the adapter already exists it only changes item list in the adapter, which happens with Observable or LiveData
* items. If the adapter is a pager adapter it does nothing.
*
* @author eMan s.r.o.
* @author eMan a.s.
* @since 0.8.0
*/
@BindingAdapter(
Expand Down Expand Up @@ -46,7 +47,7 @@ fun <T : Any> bindRecyclerView(
itemLongClickListener = longClickListener,
limit = limit
)
recyclerView.adapter = buildAdapter(config, paging, differ)
recyclerView.adapter = buildBindingAdapter(config, paging, differ)
} else {
@Suppress("UNCHECKED_CAST")
adapter as BindingRecyclerViewAdapter<T>
Expand All @@ -55,11 +56,12 @@ fun <T : Any> bindRecyclerView(
}

/**
* Binds adapter to [ViewPager2] using [BindingRecyclerViewAdapter]. Parameters are used to create this adapter and
* support item clicks, differences and other options. If the adapter already exists it only changes item list in
* the adapter, which happens with Observable or LiveData items.
* Binds adapter to [ViewPager2] using [BindingRecyclerViewAdapter] or [BindingPagingRecyclerViewAdapter] (based on
* the [paging] variable). Parameters are used to create adapter and support item clicks, differences and other options.
* If the adapter already exists it only changes item list in the adapter, which happens with Observable or LiveData
* items. If the adapter is a pager adapter it does nothing.
*
* @author eMan s.r.o.
* @author eMan a.s.
* @since 0.8.0
*/
@BindingAdapter(
Expand Down Expand Up @@ -87,15 +89,31 @@ fun <T : Any> bindViewPager2(
itemClickListener = clickListener,
itemLongClickListener = longClickListener,
)
viewPager.adapter = buildAdapter(config, paging, differ)
viewPager.adapter = buildBindingAdapter(config, paging, differ)
} else {
@Suppress("UNCHECKED_CAST")
adapter as BindingRecyclerViewAdapter<T>
adapter.setItems(items)
}
}

private fun <T : Any> buildAdapter(
/**
* Builds a binding adapter based on the variables. There are three cases at the moment:
* 1) [BindingRecyclerViewAdapter] is created when paging adapter should not be used.
* 2) [BindingPagingRecyclerViewAdapter] is created when paging adapter is used and [differ] variable is not null, since
* paging adapter requires this variable to be set.
* 3) Identifies that paging adapter should be created but [differ] is missing. It logs the error and created
* [BindingRecyclerViewAdapter] which makes sure data are displayed.
*
* @param config common configuration for any biding adapter
* @param paging true when paging adapter should be created else false
* @param differ [DiffUtil] to be used in the adapter
* @author eMan a.s.
* @see BindingRecyclerViewAdapter.build
* @see BindingPagingRecyclerViewAdapter.build
* @since 0.9.0
*/
private fun <T : Any> buildBindingAdapter(
config: BindingAdapterConfig<T>,
paging: Boolean,
differ: DiffUtil.ItemCallback<T>?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,70 @@ class BindingPagingRecyclerViewAdapter<T : Any>(
) : PagingDataAdapter<T, BaseBindingAdapter.ViewHolder>(differ),
BaseBindingAdapter<T> {

/**
* @see BaseBindingAdapter.getItemInternal
* @see getItem
*/
override fun getItemInternal(position: Int): T? = getItem(position)

/**
* @see PagingDataAdapter.onCreateViewHolder
* @see BaseBindingAdapter.onCreateViewHolderInternal
*/
override fun onCreateViewHolder(viewGroup: ViewGroup, @LayoutRes layoutId: Int): BaseBindingAdapter.ViewHolder {
return onCreateViewHolderInternal(viewGroup, layoutId)
}

/**
* @see PagingDataAdapter.onBindViewHolder
* @see BaseBindingAdapter.onBindViewHolderInternal
*/
override fun onBindViewHolder(viewHolder: BaseBindingAdapter.ViewHolder, position: Int) {
onBindViewHolderInternal(viewHolder, position)
}

/**
* @see PagingDataAdapter.onViewAttachedToWindow
* @see BaseBindingAdapter.onViewAttachedToWindowInternal
*/
override fun onViewAttachedToWindow(holder: BaseBindingAdapter.ViewHolder) {
onViewAttachedToWindowInternal(holder)
}

/**
* @see PagingDataAdapter.onViewDetachedFromWindow
* @see BaseBindingAdapter.onViewDetachedFromWindowInternal
*/
override fun onViewDetachedFromWindow(holder: BaseBindingAdapter.ViewHolder) {
onViewDetachedFromWindowInternal(holder)
}

/**
* @see PagingDataAdapter.getItemViewType
* @see BaseBindingAdapter.getItemViewTypeInternal
*/
@LayoutRes
override fun getItemViewType(position: Int) = getItemViewTypeInternal(position)

/**
* Gets item count and ensures that is is not higher than the [limit] (if set). Else it just returns item count from
* super.
*
* @return [Int] item count
* @see PagingDataAdapter.getItemCount
* @see Int.coerceAtMost
*/
override fun getItemCount() = super.getItemCount().coerceAtMost(limit ?: Int.MAX_VALUE)

companion object {

/**
* Build function which creates an instance of this adapter with the specific configuration.
*
* @param config used to build this adapter
* @param differ used to build this adapter, allows to make differences between the items
* @return [BindingPagingRecyclerViewAdapter]
*/
fun <T : Any> build(config: BindingAdapterConfig<T>, differ: DiffUtil.ItemCallback<T>) =
BindingPagingRecyclerViewAdapter(
itemBinder = config.itemBinder,
Expand Down
Loading

0 comments on commit 150eb3e

Please sign in to comment.