"Favor composition over inheritance" for RecyclerView Adapters. The idea is that you define an AdapterDelegate for each view type. This delegate is responsible for creating ViewHolder and binding ViewHolder for a certain viewtype. Then you can compose your RecyclerView Adapter by registering the AdapterDelegates that you really need.
dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:4.3.0' // if you use synthetics
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0' // if you use ViewBinding
implementation 'com.hannesdorfmann:adapterdelegates4:4.3.0' // if you don't use kotlin dsl
implementation 'com.hannesdorfmann:adapterdelegates4-pagination:4.3.0' // for pagination
}
adapterDelegate - used with findViewById(). adapterDelegateLayoutContainer - used with kotlin synthetics. adapterDelegateViewBinding - used with data binding.
fun catAdapterDelegate(
itemClickedListener : (Cat) -> Unit
) = adapterDelegateLayoutContainer<Cat, Animal>(R.layout.item_cat) {
name.setClickListener { itemClickedListener(item) }
bind { diffPayloads ->
name.text = item.name
}
}
You have to specify if a specific AdapterDelegate is responsible for a specific item. Per default this is done with an instanceof check like Cat instanceof Animal. You can override this if you want to handle it in a custom way by setting the on lambda and return true or false:
adapterDelegate<Cat, Animal> (
layout = R.layout.item_cat,
on = { item: Animal, items: List, position: Int ->
if (item is Cat && position == 0)
true // return true: this adapterDelegate handles it
else
false // return false
}
){
...
bind { ... }
}
Compose your Adapter
val adapter = ListDelegationAdapter<List<Animal>>(
catAdapterDelegate(...),
dogAdapterDelegate(),
snakeAdapterDelegate()
)
Define an AdapterDelegate for each view type. An AdapterDelegate get added to an AdapterDelegatesManager. This manager is the man in the middle between RecyclerView.Adapter and each AdapterDelegate.
public class CatAdapterDelegate extends AdapterDelegate<List<Animal>> {
private LayoutInflater inflater;
public CatAdapterDelegate(Activity activity) {
inflater = activity.getLayoutInflater();
}
@Override public boolean isForViewType(@NonNull List<Animal> items, int position) {
return items.get(position) instanceof Cat;
}
@NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new CatViewHolder(inflater.inflate(R.layout.item_cat, parent, false));
}
@Override public void onBindViewHolder(@NonNull List<Animal> items, int position,
@NonNull RecyclerView.ViewHolder holder, @Nullable List<Object> payloads) {
CatViewHolder vh = (CatViewHolder) holder;
Cat cat = (Cat) items.get(position);
vh.name.setText(cat.getName());
}
static class CatViewHolder extends RecyclerView.ViewHolder {
public TextView name;
public CatViewHolder(View itemView) {
super(itemView);
name = (TextView) itemView.findViewById(R.id.name);
}
}
}
public class AnimalAdapter extends RecyclerView.Adapter {
private AdapterDelegatesManager<List<Animal>> delegatesManager;
private List<Animal> items;
public AnimalAdapter(Activity activity, List<Animal> items) {
this.items = items;
delegatesManager = new AdapterDelegatesManager<>();
// AdapterDelegatesManager internally assigns view types integers
delegatesManager.addDelegate(new CatAdapterDelegate(activity))
.addDelegate(new DogAdapterDelegate(activity))
.addDelegate(new GeckoAdapterDelegate(activity));
// You can explicitly assign integer view type if you want to
delegatesManager.addDelegate(23, new SnakeAdapterDelegate(activity));
}
@Override public int getItemViewType(int position) {
return delegatesManager.getItemViewType(items, position);
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return delegatesManager.onCreateViewHolder(parent, viewType);
}
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
delegatesManager.onBindViewHolder(items, position, holder);
}
@Override public int getItemCount() {
return items.size();
}
}
To reduce boilerplate extend either from ListDelegationAdapter if the data source the adapter displays is java.util.List<?> or AbsDelegationAdapter which is a more general one (not limited to java.util.List).
public class AnimalAdapter extends ListDelegationAdapter<List<Animal>> {
public AnimalAdapter(Activity activity, List<Animal> items) {
delegatesManager.addDelegate(new CatAdapterDelegate(activity))
.addDelegate(new DogAdapterDelegate(activity))
.addDelegate(new GeckoAdapterDelegate(activity))
.addDelegate(23, new SnakeAdapterDelegate(activity));
setItems(items);
}
}
AsyncListDifferDelegationAdapter is the equivalent to ListAdapter that uses AsyncListDiffer internally. It does calculating diff in the background thread by default and does all particular animations.
public class DiffAdapter extends AsyncListDifferDelegationAdapter<Animal> {
public DiffAdapter() {
super(DIFF_CALLBACK) // Your diff callback for diff utils
delegatesManager
.addDelegate(new DogAdapterDelegate());
.addDelegate(new CatAdapterDelegate());
}
}
The bullet proof, fast and easy to use adapter library, which minimizes developing time to a fraction.
dependencies {
implementation "com.mikepenz:fastadapter:5.1.0"
// Many helper classes available
implementation "com.mikepenz:fastadapter-extensions-expandable:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-binding:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-diff:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-drag:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-paged:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-scroll:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-swipe:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-ui:5.1.0"
implementation "com.mikepenz:fastadapter-extensions-utils:5.1.0"
}
- Core module 100% in Kotlin
- Write less code, get better results
- Highly optimized code
- Easily extensible
- Split item view and model
- Chain other Adapters
- Click / Long-Click listeners, Selection / Multi-Selection, Expandable items, Simple Drag & Drop, Headers, Footers, Filter, Endless Scroll, "Leave-Behind"-Pattern, ActionModeHelper, UndoHelper, FastScroller, Paging (via Jetpack paging lib)
Implement your item
Create a class which extends the AbstractItem.
open class SimpleItem : AbstractItem<SimpleItem.ViewHolder>() {
var name: String? = null
var description: String? = null
/** defines the type defining this item. must be unique. preferably an id */
override val type: Int get() = R.id.fastadapter_sample_item_id
/** defines the layout which will be used for this item in the list */
override val layoutRes: Int get() = R.layout.sample_item
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
class ViewHolder(view: View) : FastAdapter.ViewHolder<SimpleItem>(view) {
var name: TextView = view.findViewById(R.id.material_drawer_name)
var description: TextView = view.findViewById(R.id.material_drawer_description)
override fun bindView(item: SimpleItem, payloads: List<Any>) {
name.text = item.name
description.text = item.name
}
override fun unbindView(item: SimpleItem) {
name.text = null
description.text = null
}
}
}
Or implement item with ViewBinding.
class BindingIconItem : AbstractBindingItem<IconItemBinding>() {
var name: String? = null
override val type: Int get() = R.id.fastadapter_icon_item_id
override fun bindView(binding: IconItemBinding, payloads: List<Any>) {
binding.name.text = name
}
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?): IconItemBinding {
return IconItemBinding.inflate(inflater, parent, false)
}
}
Set the Adapter to the RecyclerView
//create the ItemAdapter holding your Items
val itemAdapter = ItemAdapter<SimpleItem>()
//create the managing FastAdapter, by passing in the itemAdapter
val fastAdapter = FastAdapter.with(itemAdapter)
//set our adapters to the RecyclerView
recyclerView.setAdapter(fastAdapter)
//set the items to your ItemAdapter
itemAdapter.add(ITEMS)
By default the FastAdapter only provides basic functionality, which comes with the abstraction of items as Item and Model. And the general functionality of adding/removing/modifying elements. To enable selections, or expandables the provided extensions need to be activated.
SelectExtension
// Gets (or creates and attaches if not yet existing) the extension from the given `FastAdapter`
val selectExtension = fastAdapter.getSelectExtension()
// configure as needed
selectExtension.isSelectable = true
selectExtension.multiSelect = true
selectExtension.selectOnLongClick = false
ExpandableExtension
// Gets (or creates and attaches if not yet existing) the extension.
val expandableExtension = fastAdapter.getExpandableExtension()
// configure as needed
expandableExtension.isOnlyOneExpandedItem = true
Filter
// Call this in onQueryTextSubmit() & onQueryTextChange() when using SearchView
itemAdapter.filter("yourSearchTerm")
itemAdapter.itemFilter.filterPredicate = { item: SimpleItem, constraint: CharSequence? ->
item.name?.text.toString().contains(constraint.toString(), ignoreCase = true)
}
Drag and drop
// First, attach ItemTouchHelper to RecyclerView.
val dragCallback = SimpleDragCallback()
val touchHelper = ItemTouchHelper(dragCallback)
touchHelper.attachToRecyclerView(recyclerView)
// Implement ItemTouchCallback interface in your Activity, and override the itemTouchOnMove() method.
override fun itemTouchOnMove(oldPosition: Int, newPosition: Int): Boolean {
DragDropUtil.onMove(fastItemAdapter.itemAdapter, oldPosition, newPosition) // change position
return true
}
val headerAdapter = ItemAdapter<Header>()
val itemAdapter = GenericItemAdapter()
// Order defines in which order the items will show up
val fastAdapter: GenericFastAdapter = FastAdapter.with(headerAdapter, itemAdapter)
recyclerView.setAdapter(fastAdapter)
Groupie is a simple, flexible library for complex RecyclerView layouts.
dependencies {
implementation "com.xwray:groupie:2.8.0"
implementation "com.xwray:groupie-kotlin-android-extensions:2.8.0" // for kotlin synthetics
implementation "com.xwray:groupie-viewbinding:2.8.0" // for databinding
}
Groupie lets you treat your content as logical groups and handles change notifications for you - think sections with headers and footers, expandable groups, blocks of vertical columns, and much more. It makes it easy to handle asynchronous content updates and insertions and user-driven content changes. At the item level, it abstracts the boilerplate of item view types, item layouts, viewholders, and span sizes.
Use a GroupAdapter anywhere you would normally use a RecyclerView.Adapter, and attach it to your RecyclerView as usual.
val adapter = GroupAdapter()
recyclerView.setAdapter(adapter)
adapter += HeaderItem()
adapter += SomeItem()
Groups are the building block of Groupie. An individual Item (the unit which an adapter inflates and recycles) is a Group of 1. You can add Groups and Items interchangeably to the adapter.
groupAdapter += HeaderItem()
groupAdapter += CommentItem()
val section = Section()
section.setHeader(HeaderItem())
section.addAll(bodyItems)
groupAdapter += section
Modifying the contents of the GroupAdapter in any way automatically sends change notifications. Adding an item calls notifyItemAdded(); adding a group calls notifyItemRangeAdded(), etc.
There are a few simple implementations of Groups within the library:
- Section, a list of body content with an optional header group and footer group. It supports diffing and animating moves, updates and other changes.
- ExpandableGroup, a single parent group with a list of body content that can be toggled hidden or shown.
Each Item declares a view layout id, and gets a callback to bind the inflated layout.
class SongItem(private val song: Song) : Item() {
override fun getLayout() = R.layout.song
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.title.text = song.title
viewHolder.artist.text = song.artist
}
}
With data binding:
public class SongItem extends BindableItem<SongBinding> {
public SongItem(Song song) {
this(song);
}
@Override public void bind(SongBinding binding, int position) {
binding.setSong(song);
}
@Override public int getLayout() {
return R.layout.song;
}
}
Library for building complex screens in a RecyclerView created by Airbnb.
dependencies {
implementation 'com.airbnb.android:epoxy:3.11.0'
}
Models are automatically generated from custom views or databinding layouts via annotation processing. These models are then used in an EpoxyController to declare what items to show in the RecyclerView.
This abstracts the boilerplate of view holders, diffing items and binding payload changes, item types, item ids, span counts, and more, in order to simplify building screens with multiple view types. Additionally, Epoxy adds support for saving view state and automatic diffing of item changes.
There are two main components of Epoxy:
- The EpoxyModels that describe how your views should be displayed in the RecyclerView.
- The EpoxyController where the models are used to describe what items to show and with what data.
Epoxy generates models for you based on your view or layout. Generated model classes are suffixed with an underscore are used directly in your EpoxyController classes.
Epoxy uses EpoxyModel objects to decide which views to display and how to bind data to them. This is similar to the popular ViewModel pattern. Models also allow you to control other aspects of the view, such as the grid span size, id, and saved state. The RecyclerView concept of stable ideas is built into Epoxy, meaning each EpoxyModel should have a unique id to identify it. This allows diffing and saved state. A model's state is determined by its equals and hashCode implementations, which is based on the value of all of the model's properties. This state is used in diffing to determine when a model has changed so Epoxy can update the view. Each model is typed with the View that it supports. The model implements buildView to create a new instance of that view type. Additionally, getViewType returns an int representing that view type for use in view recycling. When a model's state changes, and the model is bound to a view on screen, Epoxy rebinds the view with only the properties that changed. This partial rebind is more efficient than rebinding the whole view.
From custom views:
Add the @ModelView annotation on a view class. Then, add a "prop" annotation on each setter method to mark it as a property for the model.
@ModelView(autoLayout = Size.MATCH_WIDTH_WRAP_HEIGHT)
public class HeaderView extends LinearLayout {
@TextProp
public void setTitle(CharSequence text) {
titleView.setText(text);
}
}
A HeaderViewModel_ is then generated in the same package.
From databinding:
Set up your xml layouts like normal:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="title" type="String" />
</data>
<TextView
android:layout_width="120dp"
android:layout_height="40dp"
android:text="@{title}" />
</layout>
Then, create a package-info.java file in any package and add an EpoxyDataBindingLayouts annotation to declare your databinding layouts.
@EpoxyDataBindingLayouts({R.layout.header_view, ... // other layouts })
package com.airbnb.epoxy.sample;
import com.airbnb.epoxy.EpoxyDataBindingLayouts;
import com.airbnb.epoxy.R;
From this layout name Epoxy generates a HeaderViewBindingModel_.
From ViewHolders:
If you use xml layouts without databinding you can create a model class to do the binding.
@EpoxyModelClass(layout = R.layout.header_view)
public abstract class HeaderModel extends EpoxyModelWithHolder<Holder> {
@EpoxyAttribute String title;
@Override
public void bind(Holder holder) {
holder.header.setText(title);
}
static class Holder extends BaseEpoxyHolder {
@BindView(R.id.text) TextView header;
}
}
A HeaderModel_ class is generated that subclasses HeaderModel and implements the model details.
A controller defines what items should be shown in the RecyclerView, by adding the corresponding models in the desired order. The EpoxyController encourages usage similar to the popular Model-View-ViewModel and Model-View-Intent patterns. Data flows in one direction: from your state, to the EpoxyModels, to the views on the RecyclerView. User input to the views triggers callbacks that may update your state and restart the cycle. EpoxyController has a second constructor that takes two Handler instances, one for running model building and one for handling diffing. By default these use the main thread, but they can be changed to allow async work for performance improvement.
The controller's buildModels method declares which items to show. You are responsible for calling requestModelBuild whenever your data changes, which triggers buildModels to run again. Epoxy tracks changes in the models and automatically binds and updates views.
As an example, our PhotoController shows a header, a list of photos, and a loader (if more photos are being loaded). The controller's setData(photos, loadingMore) method is called whenever photos are loaded, which triggers a call to buildModels so models representing the state of the new data can be built.
public class PhotoController extends Typed2EpoxyController<List<Photo>, Boolean> {
@AutoModel HeaderModel_ headerModel;
@AutoModel LoaderModel_ loaderModel;
@Override
protected void buildModels(List<Photo> photos, Boolean loadingMore) {
headerModel
.title("My Photos")
.description("My album description!")
.addTo(this);
for (Photo photo : photos) {
new PhotoModel()
.id(photo.id())
.url(photo.url())
.addTo(this);
}
loaderModel
.addIf(loadingMore, this);
}
}
Integrating with RecyclerView
Get the backing adapter off the EpoxyController to set up RecyclerView:
MyController controller = new MyController();
recyclerView.setAdapter(controller.getAdapter());
// Request a model build whenever your data changes
controller.requestModelBuild();
// Or if you are using a TypedEpoxyController
controller.setData(myData);
If you are using the EpoxyRecyclerView integration is easier.
epoxyRecyclerView.setControllerAndBuildModels(new MyController());
// Request a model build on the recyclerview when data changes
epoxyRecyclerView.requestModelBuild();
Epoxy
https://github.com/airbnb/epoxy
https://github.com/airbnb/epoxy/wiki/Configuration
AdapterDelegates
https://github.com/sockeqwe/AdapterDelegates
FastAdapter
https://github.com/mikepenz/FastAdapter
Groupie