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.
Inferred from Official Android Documentation
Even though ourRecyclerView
container wasMATCH_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.
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.