Best Practices and Performance
When working with Sections, it is not unfrequent to ask yourself whether what you’re doing is performant, efficient, or idiomatic. The API is short and straightforward, which makes you feel like the problems have to be very subtle and hard to find. The reality is much simpler: Sections is built on top of Android’s RecyclerView and inherits all of its properties, which are common knowledge amongst Android developers. You could build your own if you need to, and it won’t be much different from what Sections gives you today.
In this document we’ll introduce the two key qualities of performance in Sections: identity and immutability. These qualities of a data model identity is what is used by Sections to define whether a section needs to be redrawn. The more unnecessary redraws, the worse your component will perform. Skipping a redraw for a component that has meaningfully changed is not acceptable.
Once you read through this document if you feel you need to know more and deeper about RecyclerView, we recommend the original articles on RecyclerView and DiffUtils, the foundations of Sections. You’ll find they have an extensive API full of flexibility and pitfalls we’ve condensed and simplified for Facebook use cases.
#
ImmutabilityImmutability is a quality of a data model, where its values cannot change over time. An example of mutable data is an ArrayList
, which can add new elements. An example of immutable data is Java’s String
, which cannot be modified in-place, and whose operations always create a new String
instance.
As a performance-oriented person your intuition may be that creating objects incurs on a penalty and mutability is obviously faster. This is certainly true for cases with small locality, such as single-class CPU-bound algorithms. For large-scale cases where concurrency is involved, the complexity added to prevent data races and other nasty behaviours heavily offsets the cost of a few additional allocations per second.
So, what can we do to minimise the number of additional allocations of immutable data models? Never request them if they’re not necessary! To do this on Sections we visit your data model to find the smallest granularity we have to request a redraw for. This is done using the data model’s identity.
#
IdentityIdentity is another quality of a data model that defines whether it is equal to another data model, or even itself. The action of comparing two data models to find the most granular differences is referred as diffing.
What the equality is for a data model is to be defined by its creator. It can be defined by its reference in memory (referencial equality), the hash of its contents (hash equality), the value of a field such as id (identifier equality), or even delegate to the equality of each of its fields (structural equality).
Let’s see one example, User
, which has a list of Friend
:
We could define the identity of User
as its reference, which implies:
Or we could define it as the structure or hash of its contents, which becomes:
Or we could just use the user
identifier as the identity:
So, choosing the right identity for your data model is key for performance.
tip
Notice that these go from more strict to less strict: reference, structure and hash, then identifier.
warning
Java classes use the equals
function to define their identity, and the default implementation delegates to referential equality. Do remember to @Override
it accordingly.
#
SingleComponentSection DiffingSingleComponentSection is built as a thin wrapper, so it follows the Component rules for identity. If a wrapped Component has not significantly changed it won’t trigger a redraw.
The diffing algorithm for Components starts by checking the referential equality of the component with ==
, then it traverses its Props and checks their structural equality with equals
.
#
DataDiffSection DiffingThe main API of Sections is a wrapper around Android’s RecyclerView
operations, calling notifications for updates, insertions and removals behind the scenes. It takes a List
of elements and uses a traversal algorithm that takes into account all the kinds of identity equality listed above by using ==
, equals
and user defined Events.
First it checks whether it needs to do a granular identity check on the data model, and if the check passes it visits the contents of the data model.
The check for granularity does referential equality with ==
, then checks identifier equality using the event OnCheckIsSameItemEvent
, and lastly it falls back to structural equality with equals
. Remember that Java’s equals
defaults to referential equality. If any of them passes, it goes one level further to check for equality on its contents using the event OnCheckIsSameContentEvent
.
These two new events OnCheckIsSameItemEvent
and OnCheckIsSameContentEvent
exist for the benefit of data models where it’s not possible to override equals directly, i.e. using GraphQL-generated models directly.
Their API is similar to other Litho Events, where a function needs to be annotated as the listener in the ComponentSpec. The model to compare has to be annotated with @FromEvent
.
This means that we’ve made the algorithm check if two BookmarkFolderItemComponentGraphQL
have the same reference with ==
, then if they have the same id
identifier, and lastly if they match their structure with equals
. If either of these checks pass, then we dig deeper to compare the structure of their bookmark field.
#
Usage pitfalls to avoidWith this knowledge in mind, we can talk about tips and pitfalls to take into account to prevent over- and under- redraws.
#
Keep Component and Section keys stableSame as with any other Component, Sections have a key that can be overloaded to the user’s benefit. If the key is unstable, meaning that it changes at a different frequency than its contents, it will result in overdraws. Double check both for the Section and the RecyclerCollectionComponent.
#
It is still just a fancy RecyclerView adapterAny bugs or unexpected behaviours inherited from the Android implementation remain. It is not uncommon to encounter a known issue, so be sure to also search the Android bugtracker.
One frequent support request asks why adding elements before the first element doesn’t scroll the view to the top on the first redraw? This is an inherited behaviour that is easily solved by requestFocus
to scroll to the current top position.
#
Sections may have different contexts than their ComponentsThis happens frequently when events are not triggered when the caller and the listener are on different ComponentContext
. Check that your container ComponentTree
and the Components you're displaying belong in the same context.
#
Usage tips to follow#
Triggering partial redraws from outside the Sections ComponentWe will use the example of a RecyclerCollectionComponent
that is acted externally, by selecting and highlighting a bookmark after it has been added from a post in another Component.
The best approach here is to rely on DataDiffSection
diffing to modify the right row. That will mean either modifying or wrapping the current data model to include a new property of whether it’s highlighted.
tip
These additional object allocations are cheaper than a full redrawn for single-row changes.
#
Triggering full redraws from outside the Sections ComponentWe will use the example of a “Night Mode“ for your RecyclerCollection
. You’d like for the background to change between both day and night modes when a button that’s not part of the RecyclerCollection
is clicked.
warning
This approach causes the list to lose its State. Prefer partial redraws wherever possible.
The easiest way of triggering a redraw from scratch is by making the color state part of the Section key. When your Component triggers the event and the algorithm traverses the view, it’ll see that the key has change and trigger a full redraw of the relevant Section.