RecyclerView Tips: How we achieved 60 FPS in Workable’s Android Recruiting App
Workable engineering

RecyclerView Tips: How we achieved 60 FPS in Workable’s Android Recruiting App

author
Android Team
posted
share

Most of us are using RecyclerView to present data to our users in list form. It’s a common thing that a RecyclerView can draw multiple layouts on its rows – which pretty much means different XML layouts, different things to allocate memory for and sometimes tricky parts where things are so messy that we have frame drops.

Workable’s Android recruiting application needs to present data in the form of a list. The core ingredient of our lists are candidates. Candidates though, are not just simple POJOs. They consist of multiple defining characteristics each of which needs to be considered before drawing a Candidate row on a RecyclerView.

We also use DataBinding – which has made our lives easier – but has pitfalls if you’re not careful about what you use it for and how you use it.
So, having said all that, let’s jump on to the main part of the story.

We started with the famous (but not friendly) Allocation Tracker included in Android Studio. Some scroll ups and scroll downs and we had a considerable sample to work on.

Analyzing the Tracker’s report it became obvious that the TableLayout we were using for a part of the Candidate’s layout was consuming too many resources. We used TableLayout to overcome some technical difficulties we had when designing the aforementioned part. But as you might already know, for every problem in layouts there is always a solution. So, LinearLayout to the rescue. Using LinearLayout and its weight factors efficiently let us overcome those design issues and free ourselves from the resource-demanding TableLayout.

<TableLayout
    android:id="@+id/candidate_browse_job_table"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:shrinkColumns="0"
    android:stretchColumns="1">
    
    <TableRow>
        <TextView
            android:id="@+id/candidate_browser_job"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            />

        <TextView
            android:id="@+id/candidate_stage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />
    </TableRow>
    
</TableLayout>

Candidate part with TableLayout before

And the result when using LinearLayout.

<LinearLayout
  android:id="@+id/candidate_browse_job_table"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:orientation="horizontal">

  <TextView
      android:id="@+id/candidate_browser_job"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      />

  <TextView
      android:id="@+id/candidate_stage"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="0"
      />

</LinearLayout>

Candidate Part after switching to LinearLayout

It might not be such a huge difference, but based on the allocations that we saved, this alone might have saved us a frame or two.

Something else that was causing a lot of allocations was that one of the things that characterise a Candidate had to be drawn in capital letters. What easier way than setting an XML flag for it on the TextView? In the end, that didn’t seem to be such a good idea. Our approach was to use Java’s String transformation .toUpperCase().

<TextView
          ...
  android:textAllCaps="true"
          ...
/>

textAllCaps attribute before 

this.snoozedText = snoozedText.toUpperCase();

 Use of .toUpperCase() after

In addition, here’s a screenshot of the generated allocations from TextView’s “textAllCaps” attribute.

1-mpzyqmyzmybkwahcksmosg

You can see here that internally the TextView is generating a new Transformation method each time:

1-tkqvfteq1teu_nufgqpxoa

That’s probably fine in a basic scenario with a static layout, but we also have to deal with scrolling, so it can create some overhead.
While it might seem like a little thing, this really played its part on the overall optimization.

Next, it was the time to utilize RecyclerView’s .onViewRecycled() method. This method lets us know when a row in RecyclerView has been recycled, so that we can load-off some not needed resources. As I mentioned before, we’re using DataBinding. That meant it’s the appropriate time to remove OnPropertyChangedCallbacks from our ViewModel and then clear the ViewModel itself from the binding. We could also clear the ImageView that was holding the Candidate’s Avatar, which we’ve previously loaded using Glide.

@Override
public void onViewRecycled(Candidates holder) {
    if(holder != null) {
        holder.binding.getCandidateVM().removePropertyChangedCallback();
        holder.binding.setCandidateVM(null);
        holder.binding.setHighlightTerm(null);
        holder.binding.setShowJobTitle(false);
        holder.binding.setShowStage(false);
        holder.binding.executePendingBindings();
        Glide.clear(holder.binding.candidateBrowserAvatar);
        holder.binding.candidateBrowserAvatar.setImageDrawable(null);
    }

    super.onViewRecycled(holder);
}

To have a smooth experience in our RecyclerViews, we’ve also used some caching tricks directly on the RecyclerView itself. We then went on and measured the FPS with the current situation. The FPS Meter was constantly showing 60 FPS! We’d reached our goal, but we couldn’t stop there. We went forward and removed the Caching tricks from the RecyclerView.

binding.fragmentCandidateBrowseList.setItemViewCacheSize(30);
binding.fragmentCandidateBrowseList.setDrawingCacheEnabled(true);
binding.fragmentCandidateBrowseList.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

 Caching tricks

Then we re-ran our tests to see how the situation was holding. To our great surprise we were still at 60 FPS! No matter what they say, that felt really good.

The final result

To conclude, Allocation Tracker has been pretty helpful for us and our Lists. Also no matter how big or small an optimization, it can always contribute to the greater optimization of your application.

This post was written by Pavlos, follow him on twitter as @tpavlos.

share

Subscribe to the Newsletter

Hiring, talent, culture, tech and trends in a 5-minute read delivered to your inbox on Thursdays.