Scrollable user interfaces are the dominant paradigm on mobile. If you've ever built an Android app, you've probably used RecyclerView to implement a scrollable list of items.
Building a list interface on Android is fairly simple: Just create a layout for the items, hook it up to a RecyclerView adapter, and you're done. Most apps are a bit more complicated than that, though.
If you add infinite scrolling to your list, you'll need to make considerations about memory usage. If your adapter has more than a few view types, you'll have to think about how to recycle views efficiently. If the list items are complex, chances are that you'll have to optimize your layouts to avoid dropping frames while scrolling.
At Facebook, we deal with some of the most extreme use cases of RecyclerView in our various feeds. Take News Feed, for example. It's an infinite scrolling list with very complex items featuring an unlimited variety of content including shared links, rich text, videos, ads, photos, and a lot more. In addition, a large number of Android engineers across multiple product teams work on News Feed for Android and land code every day.
This comes with some interesting technical challenges. How can we consistently deliver smooth scroll performance on RecyclerViews with such complex content and so many engineers collaborating on them? How can we offer a memory-efficient recycling model in a feed with virtually infinite content variations?
At this scale, it is not sustainable to rely on individual teams to come up with one-off solutions for their own products. We need scalable shared infrastructure that encapsulates the complexity of implementing these feeds efficiently so that product teams can focus on shipping exciting features to our community.
We took inspiration from React and ComponentKit to build a powerful framework that enables Android developers to implement complex RecyclerViews that are performant by default through a simple declarative API. We call it Components for Android (C4A).
C4A adopts the React programming model with unidirectional data flow. You simply declare the different states of your UI for the given immutable inputs and the framework takes care of the rest.
Maybe the best way to describe it is by showing some code:
@LayoutSpec
public class HeaderComponentSpec {
@OnCreateLayout
static ComponentLayout onCreateLayout(
ComponentContext c,
@Prop Uri imageUri,
@Prop(resType = STRING) String title,
@Prop(resType = STRING, optional = true) String subtitle) {
return Container.create(c)
.direction(ROW)
.alignItems(CENTER)
.backgroundRes(R.drawable.header_bg)
.paddingDip(ALL, 16)
.child(
FrescoImage.create(c)
.uri(imageUri)
.placeholderRes(R.drawable.avatar_placeholder)
.withLayout()
.widthRes(R.dimen.avatar_size)
.heightRes(R.dimen.avatar_size)
.marginDip(END, 16))
.child(
Container.create(c)
.direction(COLUMN)
.flexGrow(1)
.child(
Text.create(c)
.text(title)
.textColor(Color.DKGRAY)
.textSizeSp(16)
.textColorRes(R.color.user_name))
.child(
Text.create(c, R.style.SubtitleText)
.text(subtitle)
.withLayout()
.marginRes(TOP, R.dimen.subtitle_margin)))
.build();
}
}
HeaderComponentSpec
is what we call a component spec. It's just a Java class with some special annotations. At build time, the annotation processor will generate a HeaderComponent
class and a builder with methods matching the props used in the component spec. You would use HeaderComponent
like this:
HeaderComponent.create(c)
.imageUri(Uri.parse("http://example.com/image"))
.titleRes(R.string.title)
.subtitle("And this is my subtitle.")
.build();
C4A uses Flexbox, a powerful layout system widely available on the web, backed by our own open source cross-platform implementation called css-layout. HeaderComponent
looks like this on screen:
HeaderComponentSpec
feels very simple because you're dealing with a pure function with no side effects. You simply take some props and return a layout tree. That's it! This is in stark contrast with the stateful, imperative code you usually have to write on Android UIs.
Before we dive into the features of the framework, let's have a quick look at how it displays components on screen.
In C4A, layout and rendering are implemented as two independent steps: layout and mount.
The layout step is completely decoupled from Android views because C4A has its own layout system (css-layout). Framework users need only to create a layout tree (the @OnCreateLayout
method), and the rest is handled by the framework. The result of the layout step is a list of components and their respective sizes and positions.
The layout result is then used in the mount step to create an actual view hierarchy to be rendered on screen once the component becomes visible in a RecyclerView.
We leverage this architecture to implement some exciting features under the hood: asynchronous layout, flatter view hierarchies, incremental mount, and fine-grained recycling.
Asynchronous layout
Components are pure functions with immutable inputs so they are thread-safe by design. The framework isn't bound by the Android view system, so it can perform layout in a background thread without pushing the complexity of multi-threading to users.
For example, C4A can seamlessly calculate the layout of items in a RecyclerView ahead of time without blocking the UI thread. By the time the user sees an item on screen, most the heavy work has already been done.
Flatter view hierarchies
The layout tree returned from the @OnCreateLayout
method is just a blueprint of your UI with no direct coupling with Android views. This allows the framework to process the layout tree for optimal rendering performance before the component is mounted. We do this in two ways.
First, C4A can completely skip containers after layout calculation because they are irrelevant for the mount step. In the example above, there won't be a view group wrapping title and subtitle when HeaderComponent
is mounted.
Second, components can mount either a view or a drawable. In fact, most of the core widgets in the framework, such as Text and Image, mount drawables, not views.
As a result of these optimizations, HeaderComponent
would actually be rendered as a single, completely flat, view. You can see this in the following screenshot with the Show layout bounds developer option enabled.
While flatter views have important benefits for memory usage and drawing times, they are not a silver bullet for everything. C4A has a very general system to automatically "unflatten" the view hierarchy of mounted components when we want to lean on non-trivial features from Android views such as touch event handling, accessibility, or confining invalidations. For instance, the framework would automatically wrap HeaderComponent
's title text in a view if it had a click handler on it.
Incremental mount
With layout done ahead of time in RecyclerView, we know the bounds of every single component to be rendered before they become visible. Components leverage this by incrementally mounting each of their inner contents as they become visible on screen.
Incremental mount distributes the work of rendering components transparently across multiple frames making them less likely to cause jank while scrolling. This is especially relevant for RecyclerViews with complex items that would be very challenging to implement efficiently without a lot of custom optimizations.
Fine-grained recycling
RecyclerView offers a view recycling mechanism based on the notion view types, where different types of items are recycled from separate pools. This works fine in most cases, but doesn't scale well for lists with a large number of view types because RecyclerView would constantly be inflating new views for each type, causing memory overhead and scroll performance issues.
In C4A, all leaf components such as Text and Image are pooled individually under the hood. This allows us to bring a more fine-grained recycling solution to RecyclerView. As soon as the inner parts of a composite component move off screen, the framework makes them immediately available for reuse by any other item in the adapter via incremental mount.
This means C4A dynamically cross-pollinates things like text and images across all composite items in a RecyclerView and doesn't require the use of multiple view types.
Better performance
C4A is enabling developers to implement feeds that are able to seamlessly perform layout ahead of time in a background thread and render much flatter view hierarchies that are incrementally mounted using fine-grained recycling. And they get all of these fancy features for free behind a much simpler programming model!
We are seeing improved scroll performance in our Android apps with C4A. The latest version of Facebook Lite, our app for emerging markets, is now fully based on Components in devices running Jelly Bean and later versions of Android. Scroll performance is greatly improved even on lower-end phones.
We are also in the process of converting the Facebook for Android app to use Components and see similar performance improvements when converting traditional Android views to the new framework.