Skip to main content

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.

Immutability#

Immutability 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.

Identity#

Identity 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:

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"
)
]
)

We could define the identity of User as its reference, which implies:

user1 == user1 // true
user1 == user2 // false

Or we could define it as the structure or hash of its contents, which becomes:

user1.hashCode() == user1.hashCode() // true
user1.hashCode() == user2.hashCode() // true
user1.equals(user1) // true
user1.equals(user2) // true

Or we could just use the user identifier 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

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 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 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.

@OnEvent(OnCheckIsSameItemEvent.class)
protected static Boolean onCheckIsSameItemEvent(
SectionContext c,
@InjectProp SomeHelper helper,
@FromEvent BookmarkFolderItemComponentGraphQL previousItem,
@FromEvent BookmarkFolderItemComponentGraphQL nextItem) {
return previousItem.id == nextItem.id;
}
@OnEvent(OnCheckIsSameContentEvent.class)
protected static boolean onCheckIsSameContent(
SectionContext c,
@InjectProp SomeHelper helper,
@FromEvent BookmarkFolderItemComponentGraphQL previousItem,
@FromEvent BookmarkFolderItemComponentGraphQL nextItem) {
return previousItem.bookmark.equals(nextItem.bookmark);
}

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 avoid#

With 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 stable#

Same 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 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 Component#

We 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.

class BookmarkFolderItemComponentHighlight {
int highlightColor = 0;
bool isHighlight = false;
BookmarkFolderItemComponentGraphQL model;
// constructor
// equals and hashCode
}
tip

These additional object allocations are cheaper than a full redrawn for single-row changes.

Triggering full redraws from outside the Sections Component#

We 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.

@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();
}