VirtualScroller: A Titanium module supporting memory-efficient finite and infinite scrolling

Time to release something new. I’ve been working on a little project to develop an Android app. I’ve chosen Titanium because the app is simple (no network connectivity, sensor interaction, maps, etc.) and the learning curve for Titanium is low.

I had been working with the Android SDK for about a week (very frustrating), and one feature I found to be missing from Titanium is a decent way to scroll through large datasets horizontally. In my case, I had a few dozen pages that needed to be scrolled. I didn’t want to plop all of them into a ScrollableView, since that would put a strain on Android. So, I created this little module.

It is based off of this GitHub gist, though it doesn’t bear much resemblance anymore. Scroll down for links to my project.

VirtualScroller

The module I created is called VirtualScroller, and you use it like this:

// Replace the path as necessary
var VirtualScroller = require('ui/common/VirtualScroller');

var virtualScroller = VirtualScroller({
    itemCount: 10,
    getView: function(i) {
        return Titanium.UI.createLabel({
            width: Titanium.UI.FILL,
            height: Titanium.UI.FILL,
            text: "This is item " + (i + 1)
        });
    },
    infinite: false
});

window.add(virtualScroller);

I’ll steal the description from my BitBucket page, since I don’t feel like writing a new one:

VirtualScroller is an easy to use Titanium module that wraps around a ScrollableView and provides finite and infinite scrolling. All items (which are actually Views) are created on-demand, and only remain in memory when needed. The ScrollableView instance has three Views, which act as containers for the actual items.

This method permits memory-efficient scrolling of large sets of views.

So, if you have a large amount of views to scroll through, but don’t want to load them into memory until they’re needed, check out my module. It supports finite and infinite scrolling, and uses a callback to generate each view only when needed.

On BitBucket:
Wiki (instructions and information)
Downloads

Join the Conversation

34 Comments

  1. Very useful, I was working with that same gist and was making some improvements of my own when I came across this, very nice handling of that annoying premature scollend event!

  2. This code looks very useful to me so thanks for sharing.

    Did you ever look into the programatic control of forwarding to a next page? This is something that I need.

    Cheers

      1. I tried this and it seems to work fine (on iOS). It scrolls nicely and fires the call to get new page views.

        function moveNextPage() {
            // If we're at a boundary, then there is no need to modify any views. Note that rightmost boundary checking
            // is only enforced when a finite number of views exist.
            if (!$._params.infinite && (virtualIndex === $._params.itemCount - 1)) {
                return;
            }
            var currentPage = $.scroller.getCurrentPage();
            $.scroller.scrollToView(currentPage + 1);
        }
        
  3. Hi
    I’m looking to your module, but I cannot set up correctly the “itemCount”. I have set it up to my max nr of items, but it ignores it and keeps scrolling and I get a error when I run over my array list…
    Can you help me?
    Regards

    Luca

  4. Hi MostThinsWeb
    thank you for your help

    function ApplicationWindow(title) {
    	var self = Ti.UI.createWindow({
    		title:title,
    		backgroundColor:'#000'
    	});
    	
    	var view = Ti.UI.createView({
    		left : 0,
    		top : 0,
    		width : '100%',
    		backgroundColor : "#000",
    	});
    	
    	var opere = [];
    	var data = Ti.Database.open('database.sqlite');
    	var rows = data.execute('SELECT * FROM items');
    	
    	while (rows.isValidRow()) {
    
    		opere.push({
    			id : rows.fieldByName('id'),
    			opera : rows.fieldByName('opera'),
    			autore : rows.fieldByName('autore'),
    			anno : rows.fieldByName('anno'),
    			immagine : rows.fieldByName('immagine')
    		});
    		
    		rows.next();
    	}
    	rows.close();
    	data.close();	
    	
    	Ti.API.info("Ti ho trovato " + opere.length + " opere");
    	
    	for (i = 0; i < opere.length; i++) {
    	   Ti.API.info(opere[i].immagine);
        }
    	
    	
    	var maxNrItem = opere.length;
    	
    	var VirtualScroller = require('ui/common/VirtualScroller');
    	 
    	var virtualScroller = VirtualScroller({
    	    itemCount: maxNrItem,
    	    start: 0,
    	    getView: function(i) {
    	    	
    	    	var imageView = Ti.UI.createImageView({
    	    		width: Titanium.UI.FILL,
    	    		image: "/artworks/" + opere [i].immagine
    	    	});
    	    	 Ti.API.info("/artworks/" + opere [i].immagine);
    	    	
    	    	return imageView;
    	    },
    	    isInfinite: false
    	});
    	 
    	
    	self.add(virtualScroller.view);
    	
    	return self;
    };
    
    module.exports = ApplicationWindow;
    
    

    The scroller itself seems working fine. But I need to scroll only the images i get in the array ‘opere’ which is populate from the database SELECT. Actually it returns 10 item, but I can scroll over this limit and I get the following error

    [ERROR] : Script Error = ‘undefined’ is not an object (evaluating ‘opere [i].immagine’) at ApplicationWindow.js (line 51).

    Thnak you for your help

    Luca

    1. It’s not obvious to me why it shouldn’t work – have you verified that maxNrItem is accurate? Maybe try counting the number of items as you populate the array in the while loop.

      Also, try using a different variable name as the argument to getView.

  5. The wiki link provided for instructions and information says I don’t have access. Can you please provide the instruction details in your blog. I am having serious memory leaks on adding Scrollable View to my app and would like to use your module.
    Thanks.

      1. It turns out that, we had issues with Titanium or our code that causes the crash.

        We have flicker problem. We use the virtualscroller as shown in the example code, no changes, except the views are our own, views consists of images and labels.

        Our page size is about 10, it works very good in both Android and iOS. However when we scroll to the 4th page onwards, it is started flickering the view for everypage navigation.

        When moving to 4th page from 5th,

        1. Content of 4th Page is shown first.
        2. Then again content of 5th is shown (fraction of second to create flickering effect)
        3. Then immediately content 4 is shown. (in fraction of second).

          1. This crashes on the real device observed on Motorola Xoom and Nook HD+ (both seems to be mid range CPU capacity. Also found that sometimes, on iPad 4 retina virtual scroller shows up empty page. On Android we get NullPointer exception, the stacktrace goes to getOrCreateView. Tomorrow I will try to get the stacktrace if our team found solution, I will update here. Thanks

          2. [INFO] : page is 25
            [DEBUG] : AndroidRuntime: Shutting down VM
            [WARN] : dalvikvm: threadid=1: thread exiting with uncaught exception (group=0x40af61f8)
            [ERROR] : TiApplication: (main) [3280,65495] Sending event: exception on thread: main msg:java.lang.NullPointerException; Titanium 3.2.0,2013/12/20 10:57,d9182d6
            [ERROR] : TiApplication: java.lang.NullPointerException
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.handleGetView(TiViewProxy.java:474)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.getOrCreateView(TiViewProxy.java:451)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.realizeViews(TiViewProxy.java:489)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.handleGetView(TiViewProxy.java:473)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.getOrCreateView(TiViewProxy.java:451)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.realizeViews(TiViewProxy.java:489)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.handleGetView(TiViewProxy.java:473)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.getOrCreateView(TiViewProxy.java:451)
            [ERROR] : TiApplication: at org.appcelerator.titanium.proxy.TiViewProxy.forceCreateView(TiViewProxy.java:419)
            [ERROR] : TiApplication: at ti.modules.titanium.ui.widget.tableview.TiTableViewRowProxyItem.createControls(TiTableViewRowProxyItem.java:247)
            [ERROR] : TiApplication: at ti.modules.titanium.ui.widget.tableview.TiTableViewRowProxyItem.setRowData(TiTableViewRowProxyItem.java:419)
            [ERROR] : TiApplication: at ti.modules.titanium.ui.widget.tableview.TiTableViewRowProxyItem.setRowData(TiTableViewRowProxyItem.java:91)
            [ERROR] : TiApplication: at ti.modules.titanium.ui.widget.tableview.TiTableView$TTVListAdapter.getView(TiTableView.java:229)
            [ERROR] : TiApplication: at android.widget.AbsListView.obtainView(AbsListView.java:2033)
            [ERROR] : TiApplication: at android.widget.ListView.makeAndAddView(ListView.java:1772)
            [ERROR] : TiApplication: at android.widget.ListView.fillDown(ListView.java:672)
            [ERROR] : TiApplication: at android.widget.ListView.fillFromTop(ListView.java:732)
            [ERROR] : TiApplication: at android.widget.ListView.layoutChildren(ListView.java:1625)
            [ERROR] : TiApplication: at android.widget.AbsListView.onLayout(AbsListView.java:1863)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
            [ERROR] : TiApplication: at ti.modules.titanium.ui.widget.tableview.TiTableView.onLayout(TiTableView.java:568)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at org.appcelerator.titanium.view.TiCompositeLayout.onLayout(TiCompositeLayout.java:578)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at android.support.v4.view.ViewPager.onLayout(ViewPager.java:1388)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at org.appcelerator.titanium.view.TiCompositeLayout.onLayout(TiCompositeLayout.java:578)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at org.appcelerator.titanium.view.TiCompositeLayout.onLayout(TiCompositeLayout.java:578)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at org.appcelerator.titanium.view.TiCompositeLayout.onLayout(TiCompositeLayout.java:578)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at android.widget.ScrollView.onLayout(ScrollView.java:1405)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at org.appcelerator.titanium.view.TiCompositeLayout.onLayout(TiCompositeLayout.java:578)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at org.appcelerator.titanium.view.TiCompositeLayout.onLayout(TiCompositeLayout.java:578)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at android.widget.FrameLayout.onLayout(FrameLayout.java:431)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.view.ViewGroup.layout(ViewGroup.java:4224)
            [ERROR] : TiApplication: at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1628)
            [ERROR] : TiApplication: at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1486)
            [ERROR] : TiApplication: at android.widget.LinearLayout.onLayout(LinearLayout.java:1399)
            [ERROR] : TiApplication: at android.view.View.layout(View.java:11315)
            [ERROR] : TiApplication: at android.v
            [INFO] : dalvikvm: threadid=3: reacting to signal 3
            [INFO] : dalvikvm: Wrote stack traces to ‘/data/anr/traces.txt’
            [INFO] : Process: Sending signal. PID: 14467 SIG: 9

          3. Ok, thanks. It looks like this is an internal error not related to VirtualScroller (TiViewProxy appears to be the source). Not sure what could be causing a NullPointerException.

          4. We are holding up the release, this issue is consistent but in random order, I suspect something with GC or dispose or swapping level issue. I will dig deep this week.

          5. If it’s not throwing an error and only ‘flickering’ how would I get the stack trace?

            Feel free to email me directly.

          6. Sorry not sure how to get the stack trace.

            I have debugged the code and the “flickering effect” happens on line 207 & 208.

            scrollable.currentPage = targetPage;
            scrollable.views = containers;

            Line 207 cause the view to display the page just scrolled from and 208 corrects that error.

            Does this help?

          7. Hi Vaughan,

            Unfortunately those two lines are integral to the plugin. The plugin essentially works by shuffling around the views in the underlying ScrollableView, so I fear that some flickering is unavoidable. I have no idea why it’s a problem on some devices but not others, however, so maybe truly flicker free scrolling is possible on every device. But I especially have not been able to debug the plugin in iOS, since I don’t own an iPhone or iPod touch.

            I wish I could offer more help than this; but I will say, if you get a chance to tinker with the code and figure out a flicker-free setup for your advice, let me know. There may be something I’m overlooking.

          8. Ok the flicker happens because the page pointer is updated to point to the pervious page for a second before the views are updated.

            What if you first updated the views as the new page is in there twice, once in the new and once is the old position, then update the page pointer and then finally tidy the views to remove the duplicate page..

            Make sense?

          9. I see what you mean, but I’m not sure if that will remove the flicker. Somewhere in there, I need to change the index of the page that is currently being viewed. That means the ScrollableView will still have to redraw the screen (since it doesn’t know the page I’m requesting is the same as the one it’s currently displaying). It’s worth a shot to play around with it, though, to see if I can find a workaround. I probably won’t get a chance until the summer though :(.

          10. No sorry, I was thinking I would work around the issue by increasing the number of items in the stack and reordering them less often.. Thus reducing the freq of the flicker..

            But never got around to it..

  6. Hey, great module!

    I need your suggested feature of “Infinite scrolling currently exists only in one direction. View indexing starts at 0 and goes to infinity. I’d like to implement bidirectional infinite scrolling”

    Would you be interested in developing this? I would be keen to arrange something. Please let me know your thoughts.

    Thanks!

    1. Thanks! Glad you like it. I’d have to do some thinking about this… It’s not as simple as it seems, but yes I’d be interested in adding this feature. Not sure about time frame.

Leave a comment

Leave a Reply to Luca Cancel reply