React native multi-column FlatList insert banner

Viewed 3926

I'm using multi-column FlatList in my React Native application to display items like below (left image). I'm trying to integrate AdMob banner into the application like many other apps did, and insert the ads banner in the middle of the list, like below (right image).

As far as I can tell, FlatList doesn't support this type of layout out-of-the-box. I'm wondering what would be a good practice to implement this feature and doesn't impact app performance.

(Side note, the list supports pull-to-refresh and infinite loading when approaching end of the list.).

Thank you in advance for any suggestions.

enter image description here

2 Answers

In such a case, I always recommend to drop the numColumns property and replace it by a custom render function, which handles the columns by its own.

Let's say we have the following data structure:

const DATA = 
[{ id: 1, title: "Item One"}, { id: 2, title: "Item Two"}, { id: 3, title: "Item Three"}, 
{ id: 4, title: "Item Four"}, { id: 5, title: "Item Five"}, { id: 6, title: "Item Six"}, 
{ id: 7, title: "Item Seven"}, { id:8, title: "Item Eight"}, { id: 9, title: "Item Nine"}, 
{ id: 10, title: "Item Ten"}, { id: 11, title: "Item eleven"}, 
{ id: 12, title: "Item Twelve"}, { id: 13, title: "Item Thirteen"}];

As I said we don't use the numColumns property instead we are restructuring our data so we can render our list how we want. In this case we want to have 3 columns and after six items we want to show an ad banner.

Data Modification:

  modifyData(data) { 
    const  numColumns = 3;
    const addBannerAfterIndex = 6;
    const arr = [];
    var tmp = [];
    data.forEach((val, index) => {
      if (index % numColumns == 0 && index != 0){
        arr.push(tmp);
        tmp = [];
      }
      if (index % addBannerAfterIndex == 0 && index != 0){
        arr.push([{type: 'banner'}]);
        tmp = [];
      }
      tmp.push(val);
    });
    arr.push(tmp);
    return arr; 
  }

Now we can render our transformed data:

Main render function:

render() {
    const newData = this.modifyData(DATA); // here we can modify the data, this is probably not the spot where you want to trigger the modification 
    return (
      <View style={styles.container}>
        <FlatList 
        data={newData}
        renderItem={({item, index})=> this.renderItem(item, index)}
        /> 
      </View>
    );
}

RenderItem Function:

I removed some inline styling to make it more clearer.

renderItem(item, index) {
    // if we have a banner item we can render it here 
    if (item[0].type == "banner"){
      return (
         <View key={index} style={{width: WIDTH-20, flexDirection: 'row'}}>
        <Text style={{textAlign: 'center', color: 'white'}}> YOUR AD BANNER COMPONENT CAN BE PLACED HERE HERE </Text>
      </View>
      )
    }

    //otherwise we map over our items and render them side by side 
     const columns = item.map((val, idx) => {
      return (
        <View style={{width: WIDTH/3-20, justifyContent: 'center', backgroundColor: 'gray', height: 60, marginLeft: 10, marginRight: 10}} key={idx}>
          <Text style={{textAlign: 'center'}}> {val.title} </Text>
        </View>
      )
    });
    return (
      <View key={index} style={{width: WIDTH, flexDirection: 'row', marginBottom: 10}}>
      {columns}
      </View>
    )
  }

Output:

output

Working Example:

https://snack.expo.io/SkmTqWrJS

I'd recommend this beautiful package https://github.com/Flipkart/recyclerlistview

Actually, we were handling thousands of data list in our app, flatlist was able to handle it quite good, but still we were looking for a high performance listview component to produce a smooth render and memory efficient as well. We stumbled upon this package. Trust me it's great.

Coming to your question, this package has the feature of rendering multiple views out-of-the-box. It has got a good documentation too.

So basically, the package has three important step to setup the listview.

  • DataProvider - Constructor function the defines the data for each element
  • LayoutProvider - Constructor function that defines the layout (height / width) of each element
  • RowRenderer - Just like the renderItem prop in flatlist.

Basic code looks like this:

import React, { Component } from "react";
import { View, Text, Dimensions } from "react-native";
import { RecyclerListView, DataProvider, LayoutProvider } from "recyclerlistview";

const ViewTypes = {
    FULL: 0,
    HALF_LEFT: 1,
    HALF_RIGHT: 2
};

let containerCount = 0;

class CellContainer extends React.Component {
    constructor(args) {
        super(args);
        this._containerId = containerCount++;
    }
    render() {
        return <View {...this.props}>{this.props.children}<Text>Cell Id: {this._containerId}</Text></View>;
    }
}


export default class RecycleTestComponent extends React.Component {
    constructor(args) {
        super(args);

        let { width } = Dimensions.get("window");

        //Create the data provider and provide method which takes in two rows of data and return if those two are different or not.


        let dataProvider = new DataProvider((r1, r2) => {
            return r1 !== r2;
        });

        //Create the layout provider
        //First method: Given an index return the type of item e.g ListItemType1, ListItemType2 in case you have variety of items in your list/grid

        this._layoutProvider = new LayoutProvider(
            index => {
                if (index % 3 === 0) {
                    return ViewTypes.FULL;
                } else if (index % 3 === 1) {
                    return ViewTypes.HALF_LEFT;
                } else {
                    return ViewTypes.HALF_RIGHT;
                }
            },
            (type, dim) => {
                switch (type) {
                    case ViewTypes.HALF_LEFT:
                        dim.width = width / 2;
                        dim.height = 160;
                        break;
                    case ViewTypes.HALF_RIGHT:
                        dim.width = width / 2;
                        dim.height = 160;
                        break;
                    case ViewTypes.FULL:
                        dim.width = width;
                        dim.height = 140;
                        break;
                    default:
                        dim.width = 0;
                        dim.height = 0;
                }
            }
        );

        this._rowRenderer = this._rowRenderer.bind(this);

        //Since component should always render once data has changed, make data provider part of the state


        this.state = {
            dataProvider: dataProvider.cloneWithRows(this._generateArray(300))
        };
    }

    _generateArray(n) {
        let arr = new Array(n);
        for (let i = 0; i < n; i++) {
            arr[i] = i;
        }
        return arr;
    }

    //Given type and data return the view component

    _rowRenderer(type, data) {
        //You can return any view here, CellContainer has no special significance
        switch (type) {
            case ViewTypes.HALF_LEFT:
                return (
                    <CellContainer style={styles.containerGridLeft}>
                        <Text>Data: {data}</Text>
                    </CellContainer>
                );
            case ViewTypes.HALF_RIGHT:
                return (
                    <CellContainer style={styles.containerGridRight}>
                        <Text>Data: {data}</Text>
                    </CellContainer>
                );
            case ViewTypes.FULL:
                return (
                    <CellContainer style={styles.container}>
                        <Text>Data: {data}</Text>
                    </CellContainer>
                );
            default:
                return null;
        }
    }

    render() {
        return <RecyclerListView layoutProvider={this._layoutProvider} dataProvider={this.state.dataProvider} rowRenderer={this._rowRenderer} />;
    }
}
const styles = {
    container: {
        justifyContent: "space-around",
        alignItems: "center",
        flex: 1,
        backgroundColor: "#00a1f1"
    },
    containerGridLeft: {
        justifyContent: "space-around",
        alignItems: "center",
        flex: 1,
        backgroundColor: "#ffbb00"
    },
    containerGridRight: {
        justifyContent: "space-around",
        alignItems: "center",
        flex: 1,
        backgroundColor: "#7cbb00"
    }
};

In the LayoutProvider, you can return multiple type of view based on the index or you can add a viewType object in your data array, render views based on that.

this._layoutProvider = new LayoutProvider(
            index => {
                if (index % 3 === 0) {
                    return ViewTypes.FULL;
                } else if (index % 3 === 1) {
                    return ViewTypes.HALF_LEFT;
                } else {
                    return ViewTypes.HALF_RIGHT;
                }
            },
            (type, dim) => {
                switch (type) {
                    case ViewTypes.HALF_LEFT:
                        dim.width = width / 2;
                        dim.height = 160;
                        break;
                    case ViewTypes.HALF_RIGHT:
                        dim.width = width / 2;
                        dim.height = 160;
                        break;
                    case ViewTypes.FULL:
                        dim.width = width;
                        dim.height = 140;
                        break;
                    default:
                        dim.width = 0;
                        dim.height = 0;
                }
            }
        );

tl;dr: Check the https://github.com/Flipkart/recyclerlistview and use the layoutProvider to render different view.

Run the snack: https://snack.expo.io/B1GYad52b

Related