A RecyclerView is a View that allows you to display a scrollable list of items in an app. It’s designed in a way to minimise resource usage of your device (such as memory), but that design also makes automation of a RecyclerView a bit harder. In this blog post I will teach you how to easily solve this.
How do RecyclerViews work?
Before we get started with automation, let’s take a look at a RecyclerView. A RecyclerView:
- Displays a scrollable list of items
- A ViewHolder class responsible for displaying items
- Limited number of ViewHolders
- Adapter binds new items to ViewHolder
- An item represents data and layout
A RecyclerView is used to display a list of data. We call the data combined with the Layout of Views we use to display it, an item.
A screen has limited space to display these items, so it is a waste of memory to load and create objects for all these items if they are not going to be displayed at that moment.
That is why RecyclerViews use ViewHolders. ViewHolders are in charge of displaying a single item (in this example, one Espresso beverage listing) and the amount of ViewHolders is limited to the amount needed to fill the screen (and a few more, as buffer). An adapter class holds all the items, and as you scroll through the list the adapter binds new items to the ViewHolders.
Automating the RecyclerView
Now that you know what a RecyclerView is, and how it works, it is time to start automating!
The problem with automating RecyclerViews
The challenge with automating RecyclerViews, is that you can only find and access Views that are part of the View hierarchy. And the only Views in the hierarchy for a RecyclerView, are those that have been bound to a ViewHolder. Luckily there is a solution available.
The solution lies in the Adapter that is used to bind the data to the ViewHolders. The Adapter has access to all the items, so instead of searching the View hierarchy for a certain item, we can search the Adapter. If the item is present, we need to get the RecyclerView to scroll to that position, so that the data that we are looking for gets bound to the ViewHolders.
Performing the steps outlined in the solution, is actually fairly easy thanks to a class that is available to do exactly that:
RecyclerViewActions allow us to scroll the RecyclerView to a specific position, but it also allows us to scroll to a specific item that we can specify using ViewMatchers. The code looks like this:
onView(Matcher for the RecyclerView itself).perform(RecyclerViewActions.actionOnItem(VIEW_MATCHER, VIEW_ACTION))
Example of actual implementation where we want to click an item in the RecyclerView:
onView(withId(R.id.RecyclerViewId)).perform(RecyclerViewActions.actionOnItem(withText("list item text"), click()))
And the RecyclerView will automatically be scrolled to the item with text “list item text” and then clicked.
The Trick: A Good Matcher
Creating a Matcher that works for the item that you are looking for can still be a bit tricky if you don’t really understand how the RecyclerViewActions work.
I have seen quite a few answers to RecyclerView related questions on Stack Overflow that start talking about creating custom Matchers, while in reality most items are accessible with the standard Matchers, as long is you understand how to create a good Matcher.
The RecyclerViewActions class has a few different methods that allow us to do two things: scroll somewhere, or scroll somewhere and perform an action. This can be done by providing a position, a Matcher for an item, or a Matcher for a ViewHolder. The available methods (argument type in italics):
- actionOnHolderItem(Matcher<ViewHolder>, ViewAction)
- actionOnItem(Matcher<View>, ViewAction)
- actionOnItemAtPosition(int, ViewAction)
Providing a Position
Providing a position is pretty easy. If there is a list of 20 items, and you want to click an item at a certain position:
RecyclerViewActions.actionOnItem() and RecyclerViewActions.actionOnHolderItem() require a bit more explanation.
Matching a ViewHolder
The actionOnHolderItem method should be used when you would like to perform an action on the item in a specific ViewHolder. This method (and the Matcher that you need to provide) is all about the ViewHolder! If you want to do something based on the contents of an item, you should use the actionOnItem() method, not this one. After all there are a limited number of ViewHolders, and the ViewHolders only know about the items that have been bound to it.
An example use case would be using a custom ViewHolder class that has a method ‘isInTheMiddle()’ that tracks the ViewHolders’ position on the screen. You could then use the actionOnHolderItem() method to perform an action on the item that is currently bound to the ViewHolder that is in the middle. So you are performing an action on an item, based on the properties of its ViewHolder, not its contents. Usage here is a bit limited in my opinion, and most of the time you will want to perform an action based on the contents of the item.
Matching an Item
There are two important things to keep in mind when you are creating a Matcher to use with the actionOnItem method:
- As the items in the RecyclerView use the same Layout, they will also have the same IDs. This means that finding a specific item just by providing an ID will not work. You will have to find an attribute that makes the item unique, such as text.
- You have to create a matcher that matches the whole item. This means that you have to create a Matcher that works from the perspective of the ‘root View’ in the item layout. For Example, this is the ‘blueprint’ for a RecyclerView item:
This Layout contains a ViewGroup, that in turn contains an ImageView, and two TextViews. If we would try to match this by supplying the matcher ‘withText(“Espresso”)’ it wouldn’t work. That is because there is no item with the TextView that says “Espresso”. There is an item with a ViewGroup that contains a TextView that says “Espresso”. So the correct Matcher for the item would be: hasDescendant(withText(“”Espresso)).
If the TextView is not nested in a ViewGroup, then the ‘withText(“Espresso”)’ Matcher would work.
onView(withId(R.id.RecyclerView)).perform(RecyclerViewActions.actionOnItem(withText("example text"), click())) onView(withId(R.id.RecyclerView)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("example text")), click())) onView(withId(R.id.RecyclerView)).perform(RecyclerViewActions.actionOnItem(hasDescendant(allOf(withId(R.id.example_id), withText("example text")), click()))
Official Android documentation about RecyclerViewActions:
Official Android documentation about List Automation:
Official Android documentation about RecyclerViews:
You will need to add the ‘espresso-contrib’ dependency to your project in order to use RecyclerViewActions.