When RecyclerView couldn’t hold onto the weights of our LinearLayouts

While testing our android app on tablets a while ago, a developer reported that the app had a glitch on the Search Results in landscape mode. After making a search, when we scroll on the search results page, the screen becomes unresponsive.

We immediately started investigation and were easily able to reproduce this behavior on multiple real devices as well as on emulators. So it was not device specific and the amount of memory on the device was for sure not a concern.

The first logical step that came to our mind was to profile and see if we can spot a bottleneck in the CPU Usage or Memory Usage monitors.

While memory profiler showed us a lot of things that we were tempted to look into, one of the devs said we must first look at the CPU usage because it seems some heavy computation is happening on the main thread that is rendering the app completely unusable. We moved to the CPU usage flame chart in unison. 

Here we found out that the RecyclerView was generating more Views than needed. It was then that we figured, for the initial load of the screen, we need 5 different types of views which the RecyclerView might want to already create and cache. What we saw didn’t look very nice. Here is the picture of the Call chart.

The first blue colored call that you see when viewing from top to bottom is the call to onCreateViewHolder method. Something kept on triggering the call to this method infinitely. 

We started playing with RecyclerView API to control view cache size but to no avail.

We also tried to look into our ViewHolder to find out if “maybe” we were doing something nasty there but couldn’t find anything that contributed to this problem.

While reading the code in the fragment, we stumbled upon an issue reported on Google, titled RecyclerView notifyItemChanged Prevent Scroll.

Here is the code snippet which led us to that link:

recyclerView.setHasFixedSize(true);
// In tablet landscape mode the RecyclerView inflated all views because of the bug :
// This was also cause by the upgrade to support library 23.3 and 23.4
// If this bug is fixed by Google we might use the default for setAutoMeasureEnabled to true again if that is better.
srLayoutManager.setAutoMeasureEnabled(false);

Apparently, due to some changes in the past releases of RecyclerView something changed again and even, this fix stopped working.

We then went to the XML file of the Activity in which the two fragments were being loaded on the tablet. 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="horizontal">

   <FrameLayout
      android:id="@+id/searchresults_list"
      android:layout_width="0px"
      android:layout_height="match_parent"
      android:layout_weight="1"/>

   <View
      android:layout_width="1dp"
      android:layout_height="match_parent"
      android:background="@color/bui_color_grayscale_light"/>

   <FrameLayout
      android:id="@+id/searchresults_map"
      android:layout_width="0px"
      android:layout_height="match_parent"
      android:layout_weight="1"/>

</LinearLayout>

A lot of you might have understood the problem by now, right? But I’ll still continue for those who haven’t.

RecyclerView’s parent was measuring it with unlimited width and height spec. So the parent was literally asking the RecyclerView to layout as many items as it can, which it did – well, at least tried to. 

The problem is that we were using weighted width on a horizontal weighted linear layout. To calculate the weight distribution, it measured both children with unlimited space, then distributed the remaining space. 
Even though our RecyclerView container was MATCH_PARENT because it was a horizontal linear layout and baseline align is set to true (by default, it is true), LinearLayout tried to measure children’s height to be able to align them, thus, measuring the child with unlimited height. 

Inferred from Official Android Documentation

This is not really a bug in the RecyclerView API. LinearLayout could be more clever not to do this when the child is match_parent but since we were setting baseline align in match_parent children, it was also inconsistent. 

We wrote another variant of the same screen using RelativeLayout which fixed the issue for us.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical">

   <FrameLayout
      android:id="@+id/searchresults_list"
      android:layout_alignParentStart="true"
      android:layout_toStartOf="@+id/searchresults_tablet_divider"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>

   <View
      android:id="@+id/searchresults_tablet_divider"
      android:layout_width="1dp"
      android:layout_height="match_parent"
      android:layout_centerInParent="true"
      android:background="@color/bui_color_grayscale_light"/>

   <FrameLayout
      android:id="@+id/searchresults_map"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_alignParentEnd="true"
      android:layout_toEndOf="@+id/searchresults_tablet_divider"/>

</RelativeLayout>

Here is the call chart we saw while running the app with the new variant of the Layout.

The number of calls to onCreateViewHolder reduced significantly and RecyclerView only drew what we expected it to.

This is how we solved this major bottleneck in the tablet version of the app.

In the end, I’d like to conclude that the Android Studio Monitors for Profiling CPU and Memory Usage can be very handy. Never underestimate the power of profiling your code, you’ll always find something to improve there. But be careful as it can be very tempting to fall into traps of code that doesn’t really need optimization. 

Posted In:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.