Makes the use of RecyclerView easier, modular and less error-prone.
Standard RecyclerView.Adapter
is tedious to work with, because you have to write repetitive boilerplate and spaghetti code and to concentrate all your items view logic and binding into the adapter itself, which is really bad. This library was born to be able to have the following for each element in a recycler view:
- an XML layout file, in which to define the item's view hierarchy
- a view model file (called
Item
), in which to specify the binding between the model and the view and in which to handle user interactions with the item.
In this way every item of the recycler view has its own set of files, resulting in a cleaner and easier to maintain code base.
- Setup
- Basic usage tutorial
- Adding different kind of items
- Empty item
- Filter items
- Sort items
- Using ButterKnife
- Reorder items with drag & drop
- Handle clicks
- Handle item status
- Event lifecycle
- Leave Behind pattern
- Contributors
In your gradle dependencies add:
compile 'net.gotev:recycleradapter:1.7'
In your layout resource file or where you want the RecyclerView
(e.g. activity_main.xml
) add the following:
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Create your item layout (e.g. item_example.xml
). For example:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/textView" />
</LinearLayout>
public class ExampleItem extends AdapterItem<ExampleItem.Holder> {
private String text;
public ExampleItem(String text) {
this.text = text;
}
@Override
public int getLayoutId() {
return R.layout.item_example;
}
@Override
protected void bind(ExampleItem.Holder holder) {
holder.textView.setText(text);
}
public static class Holder extends RecyclerAdapterViewHolder {
TextView textView;
public Holder(View itemView, RecyclerAdapterNotifier adapter) {
super(itemView, adapter);
textView = (TextView) findViewById(R.id.textView);
}
}
}
In your Activity (onCreate
method) or Fragment (onCreateView
method):
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
RecyclerAdapter adapter = new RecyclerAdapter();
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
recyclerView.setAdapter(adapter);
//add items
adapter.add(new ExampleItem("test"));
You can have more than one kind of item in your RecyclerView
. Just implement a different AdapterItem
for every type you want to support, and then just add it into the adapter:
adapter.add(new ExampleItem("example item"));
adapter.add(new TextWithButtonItem("text with button"));
Checkout the example app provided to get a real example in action.
It's often useful to display something on the screen when the RecyclerView is empty. To do so, simply implement a new Item
just as you would do with a normal item in the list, then:
adapter.setEmptyItem(yourEmptyItem);
wherever you need it in your code. It doesn't necessarily have to be invoked before
recyclerView.setAdapter(adapter);
If you need to search items in your recycler view, you have to override this method in each one of your items implementation:
/**
* Gets called for every item when the {@link AdapterItem#onFilter(String)}
* method gets called.
*
* @param searchTerm term to search for
* @return true if the item matches the search term, false otherwise
*/
@Override
public boolean onFilter(String searchTerm) {
return text.contains(searchTerm);
}
then, to filter the recycler view, call:
adapter.filter("search item");
and the recycler view will show only the items which matches the search term. To reset the search filter, pass null
or an empty string.
To sort items, you have the following possible approaches.
This is the recommended approach if you have to sort all your items by a single criteria and you have a list with only one type of Item
. Check compareTo JavaDoc reference for further information. In your AdapterItem
implement:
@Override
public int compareTo(AdapterItem otherItem) {
// if the other item class is not the same, then
// this item should be put before it. -1 is only
// for example
if (otherItem.getClass() != getClass())
return -1;
RobotItem item = (RobotItem) otherItem;
// in this example item, we have a field named id
// of type int, and we want to sort elements by
// id
if (id == item.id)
return 0;
return id > item.id ? 1 : -1;
}
Then call:
adapter.sort(true); //true means ascending order (A-Z), false descending (Z-A)
You can see an example in action by looking at the code in the SyncActivity
and SyncItem
of the demo app.
Your items doesn't necessarily have to implement compareTo
for sorting purposes, as you can provide also the sorting implementation out of them, like this:
adapter.sort(true, new Comparator<AdapterItem>() {
@Override
public int compare(AdapterItem itemA, AdapterItem itemB) {
if (itemA.getClass() == RobotItem.class
&& itemB.getClass() == RobotItem.class) {
RobotItem first = (RobotItem) itemA;
RobotItem second = (RobotItem) itemB;
// compare two RobotItems and return a value
}
return 0;
}
});
The first parameter indicates if you want to sort ascending (true) or descending (false). The second parameter is a custom Comparator
implementation. This is the recommended approach if you want to be able to sort your items by different criteria, as you can simply pass the Comparator
implementation of the sort type you want.
You can also combine the two techniques described above. This is the recommended approach if you have a list with different kind of items, and you want to perform different kind of grouping between items of different kind, maintaining the same sorting strategy for elements of the same type. You can implement compareTo
in everyone of your items, to sort the items of the same kind, and a custom Comparable
which will handle comparison between diffent kinds of items, like this:
adapter.sort(true, new Comparator<AdapterItem>() {
@Override
public int compare(AdapterItem itemA, AdapterItem itemB) {
// handle ordering of items of the same type with their
// internal compareTo implementation
if (itemA.getClass() == RobotItem.class
&& itemB.getClass() == RobotItem.class) {
RobotItem first = (RobotItem) itemA;
RobotItem second = (RobotItem) itemB;
return first.compareTo(second);
}
if (itemA.getClass() == PersonItem.class
&& itemB.getClass() == PersonItem.class) {
PersonItem first = (PersonItem) itemA;
PersonItem second = (PersonItem) itemB;
return first.compareTo(second);
}
// in this case, we want to put all the PersonItems
// before the RobotItems in our list
if (itemA.getClass() == PersonItem.class
&& itemB.getClass() == RobotItem.class) {
return -1;
}
return 0;
}
});
You can safely use ButterKnife in your ViewHolders. Example:
public static class Holder extends RecyclerAdapterViewHolder {
@BindView(R.id.textView)
TextView textView;
public Holder(View itemView, RecyclerAdapterNotifier adapter) {
super(itemView, adapter);
ButterKnife.bind(this, itemView);
}
}
If you are using ButterKnife in your project and you want to minimize boilerplate code in your ViewHolders, you can extend RecyclerAdapterViewHolder
, implement ButterKnife in it, and then extend all of your ViewHolders from it:
public abstract class ButterKnifeViewHolder extends RecyclerAdapterViewHolder {
public ButterKnifeViewHolder(View itemView, RecyclerAdapterNotifier adapter) {
super(itemView, adapter);
ButterKnife.bind(this, itemView);
}
}
Then you can use it like this:
public static class Holder extends ButterKnifeViewHolder {
@BindView(R.id.textView)
TextView textView;
public Holder(View itemView, RecyclerAdapterNotifier adapter) {
super(itemView, adapter);
}
}
To be able to change the items order with drag & drop, just add this line:
adapter.enableDragDrop(recyclerView);
One of the things which you may need is to set one or more click listeners to every item. How do you do that? Let's see an example.
item_example.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/secondary_text_dark"
android:id="@+id/subtitle" />
</LinearLayout>
ExampleItem.java
:
public class ExampleItem extends AdapterItem<ExampleItem.Holder> {
private Context context;
private String text;
public ExampleItem(Context context, String text) {
this.context = context;
this.text = text;
}
@Override
public int getLayoutId() {
return R.layout.item_example;
}
@Override
public boolean onEvent(int position, Bundle data) {
if (data == null)
return false;
String clickEvent = data.getString("click");
if (clickEvent != null) { // if we have a click event
if ("title".equals(clickEvent)) { //if click comes from title
Toast.makeText(context, "clicked TITLE at position " + position, Toast.LENGTH_SHORT).show();
} else if ("subtitle".equals(clickEvent)) { // or from subtitle
Toast.makeText(context, "clicked SUBTITLE at position " + position, Toast.LENGTH_SHORT).show();
}
}
return false; // Item has not changed (check event lifecycle)
}
@Override
protected void bind(ExampleItem.Holder holder) {
holder.title.setText(text);
holder.subtitle.setText("subtitle");
}
public static class Holder extends RecyclerAdapterViewHolder {
TextView title;
TextView subtitle;
public Holder(View itemView, RecyclerAdapterNotifier adapter) {
super(itemView, adapter);
title = (TextView) findViewById(R.id.title);
title.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle data = new Bundle();
data.putString("click", "title");
sendEvent(data);
}
});
subtitle = (TextView) findViewById(R.id.subtitle);
subtitle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle data = new Bundle();
data.putString("click", "subtitle");
sendEvent(data);
}
});
}
}
}
As you can see, to handle click events on a view, you have to create a click listener in the ViewHolder and propagate an event to the AdapterItem
:
title.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle data = new Bundle();
data.putString("click", "title");
sendEvent(data);
}
});
You can set whatever you want in the bundle to identify your events, together with additional data.
Then, to handle the click event:
@Override
public boolean onEvent(int position, Bundle data) {
if (data == null)
return false;
String clickEvent = data.getString("click");
if (clickEvent != null) { // if we have a click event
if ("title".equals(clickEvent)) { //if click comes from title
Toast.makeText(context, "clicked TITLE at position " + position, Toast.LENGTH_SHORT).show();
} else if ("subtitle".equals(clickEvent)) { // or from subtitle
Toast.makeText(context, "clicked SUBTITLE at position " + position, Toast.LENGTH_SHORT).show();
}
}
return false; // Item has not been changed, so return false
}
Look at the event lifecycle to have a better comprehension.
It's possible to also change the model associated to an item directly from the ViewHolder. This is useful for example to notify status changes and to persist them. Imagine we need to persist a toggle button status when the user presses on it. How do we do that? Let's see an example.
item_text_with_button.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/textView" />
<ToggleButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toggleButton" />
</LinearLayout>
TextWithButtonItem.java
:
public class TextWithButtonItem extends AdapterItem<TextWithButtonItem.Holder> {
private static final String PARAM_PRESSED = "pressed";
private String text;
private boolean pressed = false;
public TextWithButtonItem(String text) {
this.text = text;
}
@Override
public boolean onEvent(int position, Bundle data) {
pressed = data.getBoolean(PARAM_PRESSED, false);
return true;
}
@Override
public int getLayoutId() {
return R.layout.item_text_with_button;
}
@Override
protected void bind(TextWithButtonItem.Holder holder) {
holder.textView.setText(text);
holder.button.setChecked(pressed);
}
public static class Holder extends ButterKnifeViewHolder {
@BindView(R.id.textView)
TextView textView;
@BindView(R.id.toggleButton)
ToggleButton button;
public Holder(View itemView, RecyclerAdapterNotifier adapter) {
super(itemView, adapter);
}
@OnClick(R.id.toggleButton)
public void onToggleClick() {
Bundle data = new Bundle();
data.putBoolean(PARAM_PRESSED, button.isChecked());
sendEvent(data);
}
}
}
In the Holder
we have added a click listener to the ToggleButton
(in this example with ButterKnife, but you can do that also without it). When the user presses the toggle button, the RecyclerAdapter
gets notified that an event happened in a particular position:
sendEvent(data);
Then, RecyclerAdapter
calls the onEvent
method of the item which invoked sendEvent
. In this method you can update the item's internal state. If onEvent
method returns true
, it means that the Item needs to be updated. RecyclerAdapter
will call the RecyclerView.Adapter
's notifyItemChanged
method and as a result, the bind
method will be called, so your item will be updated. In this way you can safely handle the internal state of each item. If onEvent
returns false
, the event handling ends there and nothing more happens.
So, to recap, the event lifecycle is:
sendEvent(data); // send event from the ViewHolder
onEvent(int position, Bundle dataChanged); // receive event in AdapterItem
//if onEvent returns true, RecyclerAdapter invokes
//RecyclerView's notifyItemChanged method
//and the bind(Holder holder) method of the AdapterItem is called
In the demo app provided with the library, you can also see how to implement the leave behind material design pattern. All the changes involved into the implementation can be seen in this commit. This implementation has not been included into the base library deliberately, to avoid depending on external libraries just for a single kind of item implementation. You can easily import the needed code in your project from the demo app sources if you want to have leave behind implementation.
Thanks to Kristiyan Petrov for the beta testing and code review