Infinitely Scrolling Lists in Flex Applications

Have you noticed when using twitter, google plus, or certain areas of facebook that when you scroll the page, it automatically loads more data?  You don’t have to continually hit “next” to go through page after page of data. Instead, the content just “appears” as you need it. In this post we will explore a technique for making Flex list components behave in this exact way. As you scroll through the list, it continually requests more data from the server. Take a look at the video preview below, and afterwards we’ll explore the code.

The basic workflow is that you need to detect when you’ve scrolled to the bottom of the list, then load additional data to be displayed further in that list. Since you know how many records are currently in the list, you always know which “page” you are viewing. When you scroll down again, just request the next set of results that are subsequent to the last results that you requested. Each time you request data, append the list items to the data provider of the list.

First things first, you need to detect when you’ve scrolled to the bottom of the list. Here’s a great example showing how to detect when you have scrolled to the bottom of the list. You can just add an event listener to the list’s scroller viewport. Once you have a vertical scroll event where the new value is equal to the viewport max height minus the item renderer height, then you have scrolled to the end. At this point, request more data from the server.

One other trick that I am using here is that I am using conditional item renderers based upon the type of object being displayed. I have a dummy “LoadingVO” value object that is appended to the end of the list data provider. The item renderer function for the list will return a LoadingItemRenderer instance if the data passed to it is a LoadingVO.

Here it is up close, in case you missed it:

Here’s my InfiniteScrollList class:

[as3]package components
{
import model.InfiniteListModel;
import model.LoadingVO;

import mx.core.ClassFactory;
import mx.events.PropertyChangeEvent;

import spark.components.IconItemRenderer;
import spark.components.List;

import views.itemRenderer.LoadingItemRenderer;

public class InfiniteScrollList extends List
{
override protected function createChildren():void
{
super.createChildren();
scroller.viewport.addEventListener( PropertyChangeEvent.PROPERTY_CHANGE, propertyChangeHandler );
itemRendererFunction = itemRendererFunctionImpl;
}

protected function propertyChangeHandler( event : PropertyChangeEvent ) : void
{
//trace( event.property, event.oldValue, event.newValue );

if ( event.property == "verticalScrollPosition" )
{
if ( event.newValue == ( event.currentTarget.measuredHeight – event.currentTarget.height ))
{
fetchNextPage();
}
}
}

protected function fetchNextPage() : void
{
if ( dataProvider is InfiniteListModel )
InfiniteListModel( dataProvider ).getNextPage();
}

private function itemRendererFunctionImpl(item:Object):ClassFactory
{
var cla:Class = IconItemRenderer;
if ( item is LoadingVO )
cla = LoadingItemRenderer;
return new ClassFactory(cla);
}
}
}[/as3]

You may have noticed in the fetchNextPage() function that the dataProvider is referenced as an InfiniteListModel class… let’s examine this class next. The InfiniteListModel class is simply an ArrayCollection which gets populated by the getNextPage() function. Inside of the getNextPage() function, it calls a remote service which returns data to the client, based on the current “page”. In the result handler, you can see that I disable binding events using disableAutoUpdate(), remove the dummy LoadingVO, append the service results to the collection, add a new LoadingVO, and then re-enable binding events using enableAutoUpdate(). Also, notice that I have a boolean _loading value that is true while requesting data from the server. This boolean flag is used to prevent multiple service calls for the same data.

Let’s take a look at the InfiniteListModel class:

[as3]package model
{
import flash.events.Event;
import flash.utils.setTimeout;

import mx.collections.ArrayCollection;
import mx.rpc.AsyncToken;
import mx.rpc.Responder;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.RemoteObject;

public class InfiniteListModel extends ArrayCollection
{
private var _remoteObject : RemoteObject;

protected var _loading : Boolean = false;

public function get remoteObject():RemoteObject
{
return _remoteObject;
}

public function set remoteObject(value:RemoteObject):void
{
_remoteObject = value;
if ( _remoteObject )
getNextPage();
}

public function InfiniteListModel(source:Array=null)
{
super(source);
addItem( new LoadingVO() );
}

public function getNextPage() : void
{
if ( !_loading)
{
_loading = true;

trace( "fetching data starting at " + (this.length-1).toString() );
var token : AsyncToken = remoteObject.getData( this.length-1 );
var responder : Responder = new Responder( resultHandler, faultHandler );
token.addResponder( responder );
}
}

protected function resultHandler(event:ResultEvent):void
{
this.disableAutoUpdate();

if ( this.getItemAt( this.length-1 ) is LoadingVO )
this.removeItemAt( this.length-1 );

for each ( var item : * in event.result )
{
addItem( item );
}
addItem( new LoadingVO() );
this.enableAutoUpdate();

_loading = false;
}

protected function faultHandler(event:FaultEvent):void
{
trace( event.fault.toString() );
}
}
}[/as3]

Now, let’s take a look at the root view that puts everything together. There is an InfiniteScrollList whose dataProvider is an InfiniteListModel instance. The InfiniteListModel also references a RemoteObject instance, which loads data from a remote server.

[as3]<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
title="Infinite Scroll"
xmlns:model="model.*"
xmlns:components="components.*">

<fx:Declarations>
<model:InfiniteListModel id="infiniteListModel"
remoteObject="{ remoteObject }" />

<s:RemoteObject id="remoteObject"
destination="ColdFusion"
source="com.tricedesigns.infiniteScroll.Services"
endpoint="http://tricedesigns.com/flex2gateway/" />
</fx:Declarations>

<components:InfiniteScrollList
id="list"
width="100%" height="100%"
dataProvider="{ infiniteListModel }" />

</s:View>
[/as3]

Let’s not forget the remote service. In this case, I’m calling into a very basic remote CFC that returns an Array of string values. You can see the code below:

[cf]<cfcomponent>

<cffunction name="getData" access="remote" returntype="array">
<cfargument name="startIndex" type="numeric" required="yes">

<cfset items = ArrayNew(1)>
<cfloop from="1" to="25" index="i">
<cfset item = "item " & (i+startIndex)>
<cfset ArrayAppend( items, item )>
</cfloop>

<cfreturn items>
</cffunction>

</cfcomponent>
[/cf]

You can download the full source code for this application directly from here:
http://tricedesigns.com/portfolio/infiniteScroll/infiniteScroll.zip

12 replies on “Infinitely Scrolling Lists in Flex Applications”