HomeOur TeamContact

Recycler List View

By Utkarsh Lubal
Published in List
April 08, 2023
2 min read
Recycler List View

Table Of Contents

01
02
Installation
03
Tutorial

Introduction

RecyclerListView uses “cell recycling” to reuse views that are no longer visible to render items instead of creating new view objects. Creation of objects is very expensive and comes with a memory overhead which means as you scroll through the list the memory footprint keeps going up. Releasing invisible items off memory is another technique but that leads to creation of even more objects and lot of garbage collections. Recycling is the best way to render infinite lists that does not compromise performance or memory efficiency.

Apart from all performance benefits RecyclerListView comes with great features out of the box:

  • Cross Platform, works on Web
  • Supports staggered grid layouts
  • Supports variable height items even if dimensions cannot be predetermined (prop - forceNonDeterministicRendering)
  • Instant layout switching like going from GridView to ListView and vice versa
  • End reach detections
  • Horizontal Mode
  • Viewability Events
  • Initial render offset/index support
  • Footer support
  • Reflow support on container size change with first visible item preservation
  • Scroll position preservation
  • Window scrolling support for web
  • (New) ItemAnimator interface added, customize to your will how RLV handles layout changes. Allows you to modify animations that move cells. You can do things like smoothly move an item to a new position when heightof one of the cells has changed.
  • (New) Stable Id support, ability to associate a stable id with an item. Will enable beautiful add/remove animations and optimize re-renders when DataProvider is updated.
  • (New) Sticky recycler items that stick to either the top or bottom.

Props

PropRequiredParams TypeDescription
layoutProviderYesBaseLayoutProviderConstructor function that defines the layout (height / width) of each element
dataProviderYesDataProviderConstructor function the defines the data for each element
contextProviderNoContextProviderUsed to maintain scroll position in case view gets destroyed, which often happens with back navigation
rowRendererYes(type: string | number, data: any, index: number) => JSX.Element | JSX.Element[] | nullMethod that returns react component to be rendered. You get the type, data, index and extendedState of the view in the callback
initialOffsetNonumberInitial offset you want to start rendering from; This is very useful if you want to maintain scroll context across pages.
renderAheadOffsetNonumberspecify how many pixels in advance you want views to be rendered. Increasing this value can help reduce blanks (if any). However, keeping this as low as possible should be the intent. Higher values also increase re-render compute
isHorizontalNobooleanIf true, the list will operate horizontally rather than vertically
onScrollNorawEvent: ScrollEvent, offsetX: number, offsetY: number) => voidOn scroll callback function that executes as a user scrolls
onRecreateNo(params: OnRecreateParams) => voidcallback function that gets executed when recreating the recycler view from context provider
externalScrollViewNo{ new (props: ScrollViewDefaultProps): BaseScrollView }Use this to pass your on implementation of BaseScrollView
onEndReachedNo() => voidCallback function executed when the end of the view is hit (minus onEndThreshold if defined)
onEndReachedThresholdNonumberSpecify how many pixels in advance for the onEndReached callback
onEndReachedThresholdRelativeNonumberSpecify how far from the end (in units of visible length of the list) the bottom edge of the list must be from the end of the content to trigger the onEndReached callback
onVisibleIndicesChangedNoTOnItemStatusChangedProvides visible index; helpful in sending impression events
onVisibleIndexesChangedNoTOnItemStatusChanged(Deprecated in 2.0 beta) Provides visible index; helpful in sending impression events
renderFooterNo() => JSX.Element | JSX.Element[] | nullProvide this method if you want to render a footer. Helpful in showing a loader while doing incremental loads
initialRenderIndexNonumberSpecify the initial item index you want rendering to start from. Preferred over initialOffset if both specified
scrollThrottleNonumberiOS only; Scroll throttle duration
canChangeSizeNobooleanSpecify if size can change
distanceFromWindowNonumber(Depricated) Use applyWindowCorrection() API with windowShift. Usage?
applyWindowCorrectionNo(offset: number, windowCorrection: WindowCorrection) => void(Enhancement/replacement to distanceFromWindow API) Allows updation of the visible windowBounds to based on correctional values passed. User can specify windowShift; in case entire RecyclerListWindow needs to shift down/up, startCorrection; in case when top window bound needs to be shifted for e.x. top window bound to be shifted down is a content overlapping the top edge of RecyclerListView, endCorrection: to alter bottom window bound for a similar use-case. Usage?
useWindowScrollNobooleanWeb only; Layout Elements in window instead of a scrollable div
disableRecyclingNobooleanTurns off recycling
forceNonDeterministicRenderingNobooleanDefault is false; if enabled dimensions provided in layout provider will not be strictly enforced. Use this if item dimensions cannot be accurately determined
extendedStateNoobjectIn some cases the data passed at row level may not contain all the info that the item depends upon, you can keep all other info outside and pass it down via this prop. Changing this object will cause everything to re-render. Make sure you don’t change it often to ensure performance. Re-renders are heavy.
itemAnimatorNoItemAnimatorEnables animating RecyclerListView item cells (shift, add, remove, etc)
styleNoobjectTo pass down style to inner ScrollView
scrollViewPropsNoobjectFor all props that need to be proxied to inner/external scrollview. Put them in an object and they’ll be spread and passed down.
layoutSizeNoDimensionWill prevent the initial empty render required to compute the size of the listview and use these dimensions to render list items in the first render itself. This is useful for cases such as server side rendering. The prop canChangeSize has to be set to true if the size can be changed after rendering. Note that this is not the scroll view size and is used solely for layouting.
onItemLayoutNonumberA callback function that is executed when an item of the recyclerListView (at an index) has been layout. This can also be used as a proxy to itemsRendered kind of callbacks.
windowCorrectionConfigNoobjectUsed to specify is window correction config and whether it should be applied to some scroll events

For full feature set have a look at prop definitions of RecyclerListView (bottom of the file). All ScrollView features like RefreshControl also work out of the box.

applyWindowCorrection usage

applyWindowCorrection is used to alter the visible window bounds of the RecyclerListView dynamically. The windowCorrection of RecyclerListView along with the current scroll offset are exposed to the user. The windowCorrection object consists of 3 numeric values:

  • windowShift - Direct replacement of distanceFromWindow parameter. Window shift is the offset value by which the RecyclerListView as a whole is displaced within the StickyContainer, use this param to specify how far away the first list item is from window top. This value corrects the scroll offsets for StickyObjects as well as RecyclerListView.
  • startCorrection - startCorrection is used to specify the shift in the top visible window bound, with which user can receive the correct Sticky header instance even when an external factor like CoordinatorLayout toolbar.
  • endCorrection - endCorrection is used to specify the shift in the bottom visible window bound, with which user can receive correct Sticky Footer instance when an external factor like bottom app bar is changing the visible view bound.

Installation

npm install --save recyclerlistview

For latest beta:

npm install --save recyclerlistview@beta```

# Example 
###### Required files
![Recycler-List](//images.ctfassets.net/nhlw0qkjuaf2/3nqtyCBDcNKtOFsauY6H10/bc039d6768ff7e286c1d9ccc1522169d/Recycler-List_files.png)

###### “ImageRenderer.js” File

```js
import React from 'react';
import { Image, Platform, View } from 'react-native';

const isIOS = Platform.OS === 'ios';

export class ImageRenderer extends React.Component {
  shouldComponentUpdate(newProps) {
    return this.props.imageUrl !== newProps.imageUrl;
  }
  componentWillUpdate() {
    //On iOS while recycling till the new image is loaded the old one remains visible. This forcefully hides the old image.
    //It is then made visible onLoad
    if (isIOS && this.imageRef) {
      this.imageRef.setNativeProps({
        opacity: 0,
      });
    }
  }
  handleOnLoad = () => {
    if (isIOS && this.imageRef) {
      this.imageRef.setNativeProps({
        opacity: 1,
      });
    }
  };
  render() {
    return (
      <View
        style={{
          flex: 1,
          margin: 3,
          backgroundColor: 'lightgrey',
        }}>
        <Image
          ref={ref => {
            this.imageRef = ref;
          }}
          style={{
            flex: 1,
          }}
          onLoad={this.handleOnLoad}
          source={{ uri: this.props.imageUrl }}
        />
      </View>
    );
  }
}
“ViewSelector.js” File
import React from 'react';
import { Text, TouchableHighlight } from 'react-native';

export class ViewSelector extends React.Component {
  constructor(props) {
    super(props);
    this.currentView = 0;
  }
  shouldComponentUpdate(newProps) {
    return this.props.viewType !== newProps.viewType;
  }
  onPressHandler = () => {
    this.currentView = (this.currentView + 1) % 4;
    this.props.viewChange(this.currentView);
  };
  render() {
    return (
      <TouchableHighlight
        style={{
          height: 60,
          paddingTop: 20,
          backgroundColor: 'black',
          alignItems: 'center',
          justifyContent: 'space-around',
        }}
        onPress={this.onPressHandler}>
        <Text style={{ color: 'white' }}>
          Tap to Change View Type: {this.props.viewType}
        </Text>
      </TouchableHighlight>
    );
  }
}
“DataCall.js” File
export class DataCall {
  // Just simulating incremental loading, don't infer anything from here
  static async get(start, count) {
    const responseHusky = await fetch('https://dog.ceo/api/breed/husky/images');
    const responseBeagle = await fetch(
      'https://dog.ceo/api/breed/beagle/images'
    );

    const responseJsonHusky = await responseHusky.json();
    const responseJsonBeagle = await responseBeagle.json();

    const fullData = responseJsonHusky.message.concat(
      responseJsonBeagle.message
    );

    const filteredData = fullData.slice(
      start,
      Math.min(fullData.length, start + count)
    );
    return filteredData;
  }
}
“LayoutUtil.js” File
import { LayoutProvider } from 'recyclerlistview';
import { Dimensions } from 'react-native';

export class LayoutUtil {
  static getWindowWidth() {
    // To deal with precision issues on android
    return Math.round(Dimensions.get('window').width * 1000) / 1000 - 6; //Adjustment for margin given to RLV;
  }
  static getLayoutProvider(type) {
    switch (type) {
      case 0:
        return new LayoutProvider(
          () => {
            return 'VSEL'; //Since we have just one view type
          },
          (type, dim, index) => {
            const columnWidth = LayoutUtil.getWindowWidth() / 3;
            switch (type) {
              case 'VSEL':
                if (index % 3 === 0) {
                  dim.width = 3 * columnWidth;
                  dim.height = 300;
                } else if (index % 2 === 0) {
                  dim.width = 2 * columnWidth;
                  dim.height = 250;
                } else {
                  dim.width = columnWidth;
                  dim.height = 250;
                }
                break;
              default:
                dim.width = 0;
                dim.heigh = 0;
            }
          }
        );
      case 1:
        return new LayoutProvider(
          () => {
            return 'VSEL';
          },
          (type, dim) => {
            switch (type) {
              case 'VSEL':
                dim.width = LayoutUtil.getWindowWidth() / 2;
                dim.height = 250;
                break;
              default:
                dim.width = 0;
                dim.heigh = 0;
            }
          }
        );
      case 2:
        return new LayoutProvider(
          () => {
            return 'VSEL';
          },
          (type, dim) => {
            switch (type) {
              case 'VSEL':
                dim.width = LayoutUtil.getWindowWidth();
                dim.height = 200;
                break;
              default:
                dim.width = 0;
                dim.heigh = 0;
            }
          }
        );
      default:
        return new LayoutProvider(
          () => {
            return 'VSEL';
          },
          (type, dim) => {
            switch (type) {
              case 'VSEL':
                dim.width = LayoutUtil.getWindowWidth();
                dim.height = 300;
                break;
              default:
                dim.width = 0;
                dim.heigh = 0;
            }
          }
        );
    }
  }
}
“package.json” File
{
  "dependencies": {
    "recyclerlistview": "1.3.4",
    "@expo/vector-icons": "^13.0.0",
    "react-native-elements": "0.18.5"
  }
}
“App.js” File
import React, { Component } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import { RecyclerListView, DataProvider } from 'recyclerlistview';
import { DataCall } from './utils/DataCall';
import { LayoutUtil } from './utils/LayoutUtil';
import { ImageRenderer } from './components/ImageRenderer';
import { ViewSelector } from './components/ViewSelector';

export default class App extends Component {
  constructor(props) { 
    super(props);
    this.state = {
      dataProvider: new DataProvider((r1, r2) => {
        return r1 !== r2;
      }),
      layoutProvider: LayoutUtil.getLayoutProvider(0),
      images: [],
      count: 0,
      viewType: 0,
    };
    this.inProgressNetworkReq = false;
  }
  componentWillMount() {
    this.fetchMoreData();
  }
  async fetchMoreData() {
    if (!this.inProgressNetworkReq) { 
      //To prevent redundant fetch requests. Needed because cases of quick up/down scroll can trigger onEndReached
      //more than once
      this.inProgressNetworkReq = true;
      const images = await DataCall.get(this.state.count, 20);
      this.inProgressNetworkReq = false;
      this.setState({
        dataProvider: this.state.dataProvider.cloneWithRows(
          this.state.images.concat(images)
        ),
        images: this.state.images.concat(images),
        count: this.state.count + 20,
      });
    }
  }
  rowRenderer = (type, data) => {
    //We have only one view type so not checks are needed here
    return <ImageRenderer imageUrl={data} />;
  };
  viewChangeHandler = viewType => {
    //We will create a new layout provider which will trigger context preservation maintaining the first visible index
    this.setState({
      layoutProvider: LayoutUtil.getLayoutProvider(viewType),
      viewType: viewType,
    });
  };
  handleListEnd = () => {
    this.fetchMoreData();

    //This is necessary to ensure that activity indicator inside footer gets rendered. This is required given the implementation I have done in this sample
    this.setState({});
  };
  renderFooter = () => {
    //Second view makes sure we don't unnecessarily change height of the list on this event. That might cause indicator to remain invisible
    //The empty view can be removed once you've fetched all the data
    return this.inProgressNetworkReq
      ? <ActivityIndicator
          style={{ margin: 10 }}
          size="large"
          color={'black'}
        />
      : <View style={{ height: 60 }} />;
  };

  render() {
    //Only render RLV once you have the data
    return (
      <View style={styles.container}>
        <ViewSelector
          viewType={this.state.viewType}
          viewChange={this.viewChangeHandler}
        />
        {this.state.count > 0
          ? <RecyclerListView
              style={{ flex: 1 }}
              contentContainerStyle={{ margin: 3 }}
              onEndReached={this.handleListEnd}
              dataProvider={this.state.dataProvider}
              layoutProvider={this.state.layoutProvider}
              rowRenderer={this.rowRenderer}
              renderFooter={this.renderFooter}
            />
          : null}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'stretch',
    justifyContent: 'space-between',
  },
});

Tutorial

Coming Soon…


Previous Article
Shine Overlay Placeholder
Next Article
Month-Year Picker
Utkarsh Lubal

Utkarsh Lubal

Full Stack Developer

Related Posts

Basic Timeline Listview
Basic Timeline Listview
April 15, 2023
1 min

Quick Links

Advertise with usAbout UsContact Us

Social Media