Best Practice and Performance
This page covers the older Java codegen-based Sections API. If using the Kotlin Lazy Collection API, refer to the Working with Updates page in the 'Building Lists' section.
When working with Sections, it is not unusual to ask oneself whether the current design decision is performant, efficient, or idiomatic. The API is short and straightforward, which may lead to the feeling that 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.
This page introduces two key qualities of performance in Sections: immutability and identity. These qualities of a data model identity are used by Sections to define whether a section needs to be redrawn. The greater the frequency of unnecessary redraws, the worse a component will perform. However, skipping a redraw for a component that has meaningfully changed is not acceptable.
In addition to the content of this page, for a deeper understanding of RecyclerView, refer to the articles on RecyclerView and DiffUtils, the foundations of Sections. Those articles have an extensive API full of flexibility and pitfalls, which have been condensed and simplified for Facebook use cases.
Immutability​
Immutability is a quality of a data model that indicates 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.
It may seem intuitive to think that creating objects incurs a penalty and that 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 be done to minimise the number of additional allocations of immutable data models? Never request them if they’re not necessary! To do this on Sections, the data model is scrutinized to find the smallest granularity for which a redraw must be requested. This is done using the data model’s identity.
Identity​
Identity is a quality of a data model that defines whether it is equal to another data model, or even itself: this gives a measure of its 'equality'. The action of comparing two data models to find the most granular differences is known as 'diffing'.
The equality of a data model is defined by its creator, using any of the following:
- Referential equality - its reference in memory.
- Hash equality - the hash of its contents.
- Identifier equality - the value of a field (such as 'id').
- Structural equality - delegate to the equality of each of its fields (structural equality).
The following data model for 'User' has a list of Friend
, which uses an identifier (FbId
) equality:
typealias User = {
user: FbId
friends: Array<Friend>
}
typealias Friend = {
name: String
id: FbId
}
User user1 = new User(
user= "1234"
friends= [
new Friend(
name= "kata"
id= "333"
)
]
)
User user2 = new User(
user= "1234"
friends= [
new Friend(
name= "kata"
id= "333"
)
]
)
The identity of User
could be defined as its reference, which implies:
user1 == user1 // true
user1 == user2 // false
Or it could be defined as the structure or hash of its content:
user1.hashCode() == user1.hashCode() // true
user1.hashCode() == user2.hashCode() // true
user1.equals(user1) // true
user1.equals(user2) // true
Or the user
identifier could simply be used as the identity:
User user3 = new User(
user= "1234"
friends= []
)
user1.user == user1.user // true
user1.user == user2.user // true
user1.user == user3.user // true
Choosing the right identity for a data model is key to its performance.
The choices go from strict to less strict: reference, structure and hash, then identifier.
Java classes use the equals
function to define their identity, and the default implementation delegates to referential equality. Remember to @Override
it accordingly.
SingleComponentSection Diffing​
SingleComponentSection 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 Diffing​
The 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 perform a granular identity check on the data model. Then, 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 (those 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
.
@OnEvent(OnCheckIsSameItemEvent.class)
protected static Boolean onCheckIsSameItemEvent(
SectionContext c,
@FromEvent BookmarkFolderItemComponentGraphQL previousItem,
@FromEvent BookmarkFolderItemComponentGraphQL nextItem) {
return previousItem.id == nextItem.id;
}
@OnEvent(OnCheckIsSameContentEvent.class)
protected static boolean onCheckIsSameContent(
SectionContext c,
@FromEvent BookmarkFolderItemComponentGraphQL previousItem,
@FromEvent BookmarkFolderItemComponentGraphQL nextItem) {
return previousItem.bookmark.equals(nextItem.bookmark);
}
The algorithm checks 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 further analysis is triggered to compare the structure of their bookmark field.
Avoiding IndexOutOfBoundsException
​
If an IndexOutOfBoundsException
coming from RecyclerBinder
is encountered, it most likely indicates that invalid diffs are being generated by DiffUtils
.
There are two main causes for invalid diffs:
- Duplicates - according to
@OnCheckIsSameItem
, two different items in a list the are considered the same item.DiffUtils
doesn't correctly detect this and will generate an invalid diff. To fix this, ensure that no two different items in the list can ever be considered the same. To test if this is happening, temporarily enablealwaysDetectDuplicates
on theDataDiffSection
: this turns on 'pairwise duplicate checking' that will raise errors throughComponentsReporter
. - Mutable Data - since Sections diffing can occur on a background thread, mutations to the list or its elements from another thread can cause DiffUtils to emit an invalid diff. The reason for this is that
@OnCheckIsSameItem
and/or@OnCheckIsSameContent
may return inconsistent results for the same pairs of indices over the course of diffing the lists. The easiest way to fix this is to use immutable lists and immutable data with DataDiffSection and the Sections API.
With the above knowledge in mind, the following sub-sections provide pitfalls and tips to consider that will help prevent over- and under- redraws.
Usage pitfalls to avoid​
Keep Component and Section keys stable​
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 adapter​
Any 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 Components​
This happens frequently when events are not triggered when the caller and the listener are on different ComponentContext
. Check that the container ComponentTree
and the components being displayed belong in the same context.
Usage tips to follow​
Triggering partial redraws from outside the Sections Component​
Considering a RecyclerCollectionComponent
that is acted on externally by selecting and highlighting a bookmark after it has been added from a post in another Component. For such a component, the best approach is to rely on DataDiffSection
diffing to modify the correct row. That will mean either modifying or wrapping the current data model to include a new property of whether it’s highlighted.
class BookmarkFolderItemComponentHighlight {
int highlightColor = 0;
bool isHighlight = false;
BookmarkFolderItemComponentGraphQL model;
// constructor
// equals and hashCode
}
These additional object allocations are cheaper than a full redraw for single-row changes.
Triggering full redraws from outside the Sections Component​
The next example is a 'Night Mode' for the RecyclerCollection
. Here, the Developer would like the background to change between both day and night modes when a button that’s not part of the RecyclerCollection
is clicked.
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 a 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.
@OnCreateChildren
static Children onCreateChildren(final SectionContext c, @Prop int color) {
return Children.create()
.child(
DataDiffSection.<Integer>create(c)
.key("my_section_" + color)
.data(generateData(32))
.renderEventHandler(ListSection.onRender(c)))
.build();
}