Magic Touch Recycler
We have all been wowed and frustrated by the recycler…
how complex layouts can cause this janky behaviour, how to diagnose it, and then finally some tips on writing efficient layouts.
With the ever-increasing complexity of mobile applications, you would always want to avoid user reviews like “this app is janky” or “it is too slow on my device”. So today we are going to learn about how complex layouts can cause this janky behavior, how to diagnose it, and then finally some tips on writing efficient layouts.
The refresh rate of most android devices is 60fps, so we have only 1/60 = ~16 milliseconds to render a single frame to avoid any janky behavior in the app. So the optimization of even 1 millisecond can provide huge gains.
When your app processes an object for layout, the app performs the same process on all children of the layout as well. Typically, the framework executes the layout or measure stage in a single pass and quite quickly. However, some more complicated layouts may have to iterate multiple times on parts of the hierarchy that require multiple passes to resolve before ultimately positioning the elements.
Having to perform more than one layout-and-measure iteration is referred to as DOUBLE TAXATION.
Let’s take a quick look at some scenarios that lead to double taxation:
Now the question arises as to why nesting of layouts is bad? Imagine having a nested layout hierarchy in a RecyclerView item; the cost now gets multiplied by the number of items and that’s what can lead to the rendering time of a frame exceeding this 16-millisecond window.
Here are some tools that can help you diagnose the problem:
Let’s take a look at a few things we can do for optimizing layouts.
Using the <include>
tag is a very common practice. However, it can lead to unnecessary nesting. Let’s see how.
Say you defined a reusable header layout and used <include> to add this layout to an activity. We achieved code readability and reusability here—great! Let’s see how the system interprets it: the compiler replaces the <include> tag with the layout file, so we get nested Constraint-Layouts in this case.
There is a very quick and simple way to avoid this nesting by using the <merge> tag. Just replace the ConstraintLayout in layout_header.xml with the <merge> tag. To see how your views would be aligned when this layout is included in a parent layout (activity_home.xml) you can use tools: parentTag attribute to specify the parent ViewGroup in which this layout would be included, that’s it! This will make the UI in the editor look like it would when included in the specified ViewGroup.
The layout_header_optimised.xml is included the same way we included layout_header.xml earlier.
Let’s see how the system interprets it: the compiler places all the elements (header <TextView> in this case) wrapped inside the <merge> tag directly in the layout, in place of the <include> tag. The nested Constraint-Layouts we saw earlier are now gone. Poof! That’s the magic of the <merge> tag.
Sometimes double taxation and nesting come as a side effect of the parent layout you are using. Choose parent layouts wisely. As a rule of thumb I tend to follow these guidelines:
“In the N release of Android, the ConstraintLayout class provides similar functionality to RelativeLayout, but at a significantly lower cost.”
Sometimes your app uses layouts with views that are not visible on the screen when the layout is inflated. These are the views that you might want to show in the future based on user interactions. Some of the examples could be progress indicators, undo messages, item details. You can render them only when required. This will reduce memory usage and speed up rendering of the layout. How do we do that? You can defer the loading of views by using a ViewStub.
ViewStub is a lightweight view with no dimension that doesn’t draw anything or participate in the layout. As such, it's cheap to inflate and cheap to leave in a view hierarchy.
Using ViewStub is simple, you can just replace the <include> tag in your layout with ViewStub and specify an inflatedId that would be used to refer to the inflated layout when ViewStub is loaded.
When you want to load the layout specified by the ViewStub, either set it visible by calling setVisibility(View.VISIBLE) or call inflate(); however, the inflate() method has the benefit of returning the root View of the inflated layout.
It is very important to remember that after the stub is inflated, the stub is removed from the view hierarchy. So a reference to progress_stub would now return null. A ViewStub is a great compromise between ease of programming and efficiency. The only drawback of ViewStub is that it currently does not support the <merge> tag.
Let’s jot down the things we have read so far into actionable items:
All good things come at a price
Optimizing layouts is rewarding for complicated applications but there are a few pain points that make it overhead for simple apps with less complex layouts. So keep in mind the following points before you delve deep into optimizing your layouts:
While Android development is moving towards an xml-less direction with Compose, it might help to keep these tips in mind until you can completely purge xml from your codebase.
Thanks for sticking around till the end, hope you learned something! 👋
We have all been wowed and frustrated by the recycler…