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.
You can see here that internally the TextView is generating a new Transformation method each time:
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.
The post RecyclerView Tips: How we achieved 60 FPS in Workable’s Android Recruiting App appeared first on Inside Workable - news, company announcements, new features from Workable.