Infinite List, Windowed List, and RecyclerView are not new. In fact, their ideas are rather simple: render only what’s visible from the user’s viewport, recycle all item view instances with a smart backing data store, and progressively re-render dirtied items based on the scroll position delta. But details matter, and it’s not often done efficiently, regardless of platforms.
Our Use Case
Chop is a mobile commerce startup in the pre-launch stage. Since our inception we chose React Native to develop our iOS prototype both for technical and business reasons:
- We need Native features like Bluetooth, background tasks, notifications, and payment options on iOS. With the Progressive Web App the Web is getting there, it just isn’t universally available yet.
- We hope to develop on Android platform later and possibly a web version of Chop in the future. React Native allows us to to maximize code reuse across all platforms.
- The ability to quickly change the styling of the app, and move components based on customer input is critical. React Native allows faster iterations than any other platform.
React Native combines fast execution with the best use of today’s developer ecosystem — making it a perfect platform for startups.
One feature of the very early Chop prototypes is an infinite scrolling list of cafes and restaurants, with estimated wait times at each location. It has a few characteristics:
- Our list item is a fixed width and height
- We know the size of the list ahead of time
- The list should scroll fast without jank or high memory use
- The estimated wait time circle needs to be self-contained so it doesn’t dirty the entire list when there is a change
Here is the demo of our infinite list implementation:
Let’s first take a quick look how this is done on iOS natively:
Typically, if the user scrolls down, row seven will scroll off the screen, while row four will scroll in. UITableView accomplishes this by adjusting the contentOffset range. Soon, however, if you keep scrolling you will reach the edge of the content.
A few years ago, Apple introduced a technique to achieve infinite scrolling. You can watch their video describing it here: http://devstreaming.apple.com/videos/wwdc/2011/104/refmovie.mov ( Safari only, at 4:26 mark).
Simplified, the first step is to re-adjust contentOffset to its original center state so the user won’t hit the edge. However, now row four is off the screen. To bring row four back to the screen you have to move all the rows down by one. If you do both operations in the same event loop it will be transparent to the user, and allow for infinite scrolling.
React Native Implementation
React Native is using a different optimization strategy, which Christopher Chedeau, explains well in this article.
Essentially, React Native uses ListView for infinite list but it has a number of drawbacks. The most important of which is that React cannot assume elements of the list are immutable, and therefore it has to keep a reference to every new element that is scrolled into view in order to perform change detection. Regardless of whether it does detach views when their offsets are out of the visible area, its memory representation of the list items remains in memory. Diffing gets more and more expensive over time as the list grows.
Let’s take a look at its implementation.
Currently, Brent Vatne also has an experimental windowed ListView for fixed height list: https://github.com/brentvatne/fixed-height-windowed-list-view-experiment
Chop: Fixed Height Infinite List
We took a slightly different approach to better suit Chop’s functionality.
Since our application uses fixed-height rows that all follow the same template, and we know the height of our viewable area, we settled on a design that treats the ScrollView’s child elements as immutable, except for the top-level elements in the array, and the height of the ScrollView’s interior. All other attributes of the list and its items will remain constant once created.
Here’s a look at Chop’s specific implementation:
Because everything on the list is fixed in size, height and position, from React Native’s perspective there is no apparent diff. This means there is no need to hold more rows in the tree so there is no increase of the memory usage.
When the size of each item is known, you can compute the y-position of any element with a formula such as y-position = itemIndex * itemHeight. The body height of the ScrollView is always the highest y-position item plus its height.
OnScroll: How to update the renderModel
OnRender: How to rerender the Infinite Scroll Widget
The Need for a Fixed Height List
The majority of the practical use cases out there simply deal with fixed-size items. (App Store app list, iTunes music list, Yelp restaurant list, Netflix movie list, Amazon product list, YouTube video list, or a geographical map with infinite bi-directional scrolling via fixed-size tiling, etc.). A variable-size list is often needed for user generated posts, typically in social network apps such as Facebook and Twitter. However it’s not the most common use case, and the implementation should not solely account for the worst case scenario.
Hopefully, a high performance fixed-height list can be standardized in the React Native platform to handle these use cases.
Special thanks to Jacky Nguyen who was the architect of this design.