Journey to RecyclerView

The world is veiled in lists!

Jules Tréhorel
iAdvize Engineering
5 min readDec 14, 2015

--

A lot of Android applications are based on lists. Typical navigation consists in displaying a list of objects that, when clicked, opens another activity (or fragment) displaying more specific content of the object.

Some list-pattern Android apps

The Android components for such pattern are the ListView (UI) and list adapters (controller between view and model) whiches are discussed widely on the web (start here if you are a beginner).

The designer waits, his only hope: animations…

Lists implies ordering of the dataset. This ordering is quite dependent on use-case (alphabetical for music players, recentness for messaging apps…).
If your ordering logic depends on time, then be sure that someday, in a near future, a designer will come to you asking for reordering animations.
In fact, animating the reordering of your list is a good way to improve the UX of your app. Letting the user see the items movements will help him understand the direct implications of his actions. On the contrary, simply refreshing the list where all items have moved will leave him perplex about what just happened.

Reordering with/without animations

When the ListView is in darkness RecyclerView will come…

If you are familiar with ListViews you will know that they are not sufficient for such a request. When you update your model and notify the adapter of the change, the UI update is immediate.
However there is another component of the Android SDK that can do this job: RecylerView.
RecyclerView is a more flexible UI Component for displaying large sets of data with the use of a LayoutManager (“recycler” because out-of-bounds views are recycled to avoid useless object creation). This allows to decorrelate the “model position” and the “view position” of the object. This way, an object that is at the start of your model (adapter position = 0) is not necessarily at the top of your view (layout position = 0).

Levelling up!

Here again, switching from ListView to RecyclerView is a well discussed topic on the web (try here and here). There are several advantages of doing so:
- auto-recycling of views (no more needing to use the convertView)
- custom touch events on item (no more dealing with tricky cases between onListItemClicked and the item internal touch events)
- obligation to use the ViewHolder pattern (clearer code)
- custom list/grid/whatever layout (through the use of LayoutManager)
- item decorations and animations

The animation feature is the one we need here. Where traditional ListView adapters only got notifyDataSetChanged at their disposal, RecyclerView adapters are equipped with multiple notifying methods that will allow us to tweak our model updates to our animated design need.

New dungeon

However while this solution will work like a charm in a lot of use-cases (and in every tutorial on RecyclerView you can find on the web) it will require some addition to behave well in certain scenarii.

Indeed, the notify methods from RecyclerView.Adapter imply atomic changes on your model. In a scenario where the model is entirely updated you will have to compare your old and new model to search for the changes that occured and reflect them one-by-one to the adapter accordingly. After multiple unsuccessful tries here is what worked in my case:

public void update(final List<Item> newItems) {
final List<Item> oldItems = new ArrayList<>(mItems);
final List<Item> tmpItems = new ArrayList<>(oldItems);
mItems = newItems;

// 1 - Deleted items must be removed
for (int i = oldItems.size() - 1; i >= 0; i--) {
if (!newItems.contains(oldItems.get(i))) {
notifyItemRemoved(i);
tmpItems.remove(i);
}
}

// 2 - Added items must be inserted
for (int i = 0; i < newItems.size(); i++) {
final Item newItem = newItems.get(i);
if (!oldItems.contains(newItem)) {
notifyItemInserted(i);
tmpItems.add(i, newItem);
}
}

// 3 - Items that switched place must be moved
Item misplacedItem = getMisplacedItem(tmpItems, newItems);
while (misplacedItem != null) {
final int oldPosition = tmpItems.indexOf(misplacedItem);
final int newPosition = newItems.indexOf(misplacedItem);
notifyItemMoved(oldPosition, newPosition);

final Item misplaced = tmpItems.remove(oldPosition);
tmpItems.add(newPosition, misplaced);
misplacedItem = getMisplacedItem(tmpItems, newItems);
}

// 4 - Rooms with new content must be updated
for (int i = newItems.size() - 1; i >= 0; i--) {
final Item newItem = newItems.get(i);
final Item oldItem = tmpItems.get(i);
if (!oldItem.getName().equals(newItem.getName())) {
notifyItemChanged(i); // Item at i has changed
}
}
}

private Item getMisplacedItem(final List<Item> tmpItems, final List<Item> newItems) {
for (int i = 0; i < tmpItems.size(); i++) {
final Item oldItem = tmpItems.get(i);
final int newPosition = newItems.indexOf(oldItem);

if (i != newPosition)
return oldItem;
}
return null;
}

Full sample code available here : https://gist.github.com/Judas/53b44c3166c4f03348f2

Final boss

Now our quest is almost over but we still face a last issue. Let’s imagine your model updates are asynchronous and can be fired anytime, and especially when your RecyclerView is not visible: the animations would play while in the background. Back to square one…
It seems a minor problem as we would only have to delay the update until the next time the list screen is shown.

private boolean mVisible;
private List<Item> mPendingModel;

@Override
protected void onResume() {
super.onResume();
mVisible = true;

if (mPendingModel != null)
mAdapter.update(mPendingModel);
mPendingModel = null;
}

@Override
protected void onPause() {
super.onPause();
mVisible = false;
}
// Called by your asynchronous core service
private void onModelUpdate(final List<Item> newItems) {
if (!mVisible)
mPendingModel = newItems;
else
mAdapter.update(newItems);
}

However this will not work. The trick here is that the onResume is fired when the Activity comes to the foreground but this does not mean that it is visible yet. Indeed the Activity will fire the onResume event and then play an enter transition. If your RecyclerView animations are faster than this enter transition, you won’t see them (or maybe only the end if you’re lucky).

The final tip is to use the onEnterAnimationComplete event which is called when the enter transition of the activity is finished (as its name states).
Unfortunately this API is only available since SDK 21 (Lollipop). So when targeting older devices we still have to ungracefuly delay the update with an arbitrary timer.

@Override
protected void onResume() {
super.onResume();

// On older phones, post update when activity is resumed
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
mVisible = true;

if (mPendingModel != null) {
mRecyclerView.postDelayed(() -> {
mAdapter.update(mPendingModel);
mPendingModel = null;
}, 500);
}
}
}

@Override
public void onEnterAnimationComplete() {
super.onEnterAnimationComplete();

// On recent phones, update when activity transition has ended
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mVisible = true;

if (mPendingModel != null) {
mAdapter.update(mPendingModel);
mPendingModel = null;
}
}
}

Loot!

RecyclerView is a really powerful tool to create complex list/grid layouts and to animate its changes, although it needs a little tweaking when you face some borederline cases.

--

--