RecyclerView and espresso, a complicated story

Espresso is not really helpful when it comes to check items displayed with a RecyclerView. The common pattern using a ListView (or any kind of AdapterView) would be using onData:

onData(is(instanceOf(Item.class)))
.check(matches(hasDescendant(withText("Some text"))));

onData is quite convenient for ListView as it will scroll to each item and apply the matcher. Unfortunately there is no similar thing for RecyclerViews.

Using onView, it’s pretty straightforward to check views that are on the screen:

onView(withId(R.id.recycleview))
.check(matches(hasDescendant(withText("Some text"))))

Or with a matcher targeting a specific child:

onView(nthChildOf(withId(R.id.recycleview), 0)
.check(matches(hasDescendant(withText("Some text"))))

We’re getting close, but this would not work with views that are not visible. So maybe we want to scroll to the given position and then check the view at that position:

int i = 10;
onView(withId(R.id.recyclerview))
onView(viewMatcher)
.perform(scrollToPosition(i))
.check(new RecyclerItemViewAssertion<>(i, items.get(i), new ItemViewAssertion<Item>() {
@Override
public void check(Item item, View view, NoMatchingViewException e) {
matches(hasDescendant(withText(item.getDisplayName())))
.check(view, e);
}
}));

(See link at the end of this article for RecyclerItemViewAssertion and ItemViewAssertion)
This is getting a bit messy but it works pretty well. After wrapping this in a method taking a list of items we can quickly check out RecyclerView using these lines:

RecyclerViewInteraction.    
<Item>onRecyclerView(withId(R.id.recyclerview))
.withItems(items)
.check(new ItemViewAssertion<Item>() {
@Override
public void check(Item item, View view, NoMatchingViewException e) {
matches(hasDescendant(withText(item.getDisplayName())))
.check(view, e);
}
});

And voila! Of course it’s not as powerful as a good ol’ onData but it will do the job for a lot of problems.

See this gist for the full source code.