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:

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);
		}
	}
}

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:

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() );
		}
	}
}

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.

<?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>

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:

<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>

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

  • http://www.tricedesigns.com Andrew

    James Ward also has a great example using AsyncListView, which allows you to quickly scroll through massive data sets via lazy loading, available at http://www.jamesward.com/2010/10/11/data-paging-in-flex-4/

    The main difference between the two approaches is that James’ approach requires you to know the total number of records. This approach does not.

  • http://histos.net Cliff Meyers

    James approach is nice if you do happen to know the total numbers of records up front since you can “seek” to the end of the data set using the scroll thumb. Two nice posts all in one spot. Nicely done.

  • Kevin Chua

    hi there,

    From your example, you are just add the item value from the web service return.

    Can you please help on on how to customize the list item by adding the messagefield text and icon on it ?

    Appreciate your feedback.
    Thank you.

  • Samy

    Hi,

    like Kevin I can not add a message with your great component =>
    How do that with this code ?

    Thank you.

  • MJ

    Hi,

    when I try to implement the messageField or the iconField using the s:itemRenderer as described in the link , I get a:

    Could not resolve to a component implementation.

    When using the extended InfiniteScrollList List. Using a standard List works of course fine. How can I implement the message & icon fields in the custom InfiniteScrollList?

    • http://www.tricedesigns.com Andrew

      What version of Flex are you using? That sounds like an error if you are trying to use older (Flex 3) components.

  • MJ

    This is using Flex SDK 4.6.0, and in a Flex Mobile Project.

  • http://Website Swen van Zanten

    What do I need to change to let this work with PHP. I’ve Used the 4.6 buildin PHP Generator. Do I have to use the getName_paged function?

    • http://www.tricedesigns.com Andrew

      Update the getNextPage function in the InfintieListModel class to retrieve results from your PHP service.

  • Kevin Remisoski

    It shouldn’t be too difficult to create a function that returns the record count and use data binding to bind that value to the Flex control in the MXML source…