Xamarin Android - Build a Gridview With Infinite Scrolling

January 27, 2015

I needed to implement a GridView display, which had two columns of items. Each item consisted of ImageView for a small thumbnail image (160x160), above a small TextView with a basic description. Easy enough, and there’s a ton of examples out there on how to do it.

However, my list of items is extremely large (it runs into the thousands). There’s way too many items to load in one go, and even if I could, the data set is dynamic, and there’s more of them being added all the time.

This is a problem that’s already been solved by ‘infinite scrolling’ or ‘endless scrolling’. If you’ve ever used any form of mobile app or website which has a large number of items in a list, you’ll already be familiar with the concept: Basically you’re shown an initial list of items, but as you scroll down, more items are loaded automatically, and displayed. All you have to do is just keep on scrolling…

How to get this working in a GridView though?

It was surprisingly difficult to find information on this. There were tons of examples showing how to do it in a ListView with ListAdapters and ArrayAdapters, but all the ones I was looking at had a static list of items. They’d start out with a list of items, and not add any more to it. Great to demonstrate how a GridView works, but infinite scrolling… not so much.

The basic principles:

  • Load the first set of items
  • Capture something from the scrolling event which said “we’re getting close to the end, go and get more items!”
  • Load the items, hopefully quickly enough so the user doesn’t bump up against the bottom of the current list.
  • Tell the DataAdapter to notify everything attached to it that its data set has changed (coz it’s got new items in it).
  • Tell the GridView to invalidate its views (i.e. cells) which causes it to repaint everything (and update the scrollbar proportions, list count indicator, etc.)

Getting Started

To get started in this demo, we first need a backing data store which has the ability to load more items on demand. This could be a web service, an API call, a database query, an Observable collection of some sort, pretty much anything.

For the purposes of this example, I’m just going to use a generic list of MySimpleItem (as you may be able to tell from the name, it’s just a toy object).

public class MySimpleItem
{
public string DisplayName { get; set; }
}

I’m going to keep a list of these in an object which I’ve created called MySimpleItemLoader, another toy object which has a method I can call whenever I like, to get more more MySimpleItems. Easy:

public class MySimpleItemLoader
{
public List<MySimpleItem> MySimpleItems { get; private set; }
public bool IsBusy { get; set; }
public int CurrentPageValue { get; set; }
public bool CanLoadMoreItems { get; private set; }

public MySimpleItemLoader()
{
MySimpleItems = new List<MySimpleItem>();
}

public void LoadMoreItems(int itemsPerPage)
{
IsBusy = true;
for (int i = CurrentPageValue; i < CurrentPageValue + itemsPerPage; i++)
{
MySimpleItems.Add(new MySimpleItem(){DisplayName = string.Format("This is item {0:0000}", i)});
}
// normally you'd check to see if the number of items returned is less than
// the number requested, i.e. you've run out, and then set this accordinly.
CanLoadMoreItems = true;
CurrentPageValue = MySimpleItems.Count;
IsBusy = false;
}
}

Let’s just unpack this quickly:

  • The List<MySimpleItem> MySimpleItems is obviously just my data store.
  • The CurrentPageValue is just a grouping counter we can use to determine how far into the dataset we are, to get the next set (‘page’) of items.
  • The CanLoadMoreItems property is a flag to indicate whether there are more items than the ones that were just loaded.

The CanLoadMoreItems variable is pretty dimwitted. For the purposes of this demo it’s always going to be true. But normally you could do a quick check to see if the last number of items returned was less than the number you asked for; if it was, then there’s obviously no more items.

  • As for the LoadMoreItems method… I’m sure you’ll figure it out :)

So far so good.

Now we need something to act as a middle-man between the GridView and the list of MySimpleItems. Enter the DataAdapter class of object. This acts as a kind of bridge between the list of data on the one side, and the display of it, on the other. It also handles things like recycling of views to save and reuse system resources, a topic I’ll go into in a future post where I’ll show how to display more than one kind of item in the grid, speed up the display, and so on.

Setting Up The Data Adapter

There are many different kinds of DataAdapters, but we’re going to use a derivative of the standard BaseAdapter, which I called (imaginatively) MyGridViewAdapter. And here it is:

public class MyGridViewAdapter : BaseAdapter<MySimpleItem>
{
private readonly MySimpleItemLoader _mySimpleItemLoader;
private readonly Context _context;

public MyGridViewAdapter(Context context, MySimpleItemLoader mySimpleItemLoader)
{
_context = context;
_mySimpleItemLoader = mySimpleItemLoader;
}

public override View GetView(int position, View convertView, ViewGroup parent)
{
// we'll come back here in a sec...
}

public override long GetItemId(int position)
{
return position;
}

public override int Count
{
get { return _mySimpleItemLoader.MySimpleItems.Count; }
}

public override MySimpleItem this[int position]
{
get { return _mySimpleItemLoader.MySimpleItems[position]; }
}
}

First I inherit from BaseAdapter<MySimpleItem>. This just makes it a little bit easier for the data adapter, means less object casting for us, and lets the parent of the BaseAdapter know what kind of objects it’s going to have to juggle internally so it can do all the things we shouldn’t have to care about.

In the constructor we pass in an instance of the MySimpleItemLoader (which will have the first set of items already in it).

We also pass in the the Android application Context. This is only so we can use it in overridden GetView method to help us inflate the correct type of grid view item display resource for each item in the grid, as it scrolls into view. More on this in a bit.

And then we just have to override a couple of abstract methods (which the BaseAdapter will force us to do), because it’s stuff that it needs us to implement.

public override long GetItemId(int position)
{
return position;
}

public override int Count
{
get { return _mySimpleItemLoader.MySimpleItems.Count; }
}

public override MySimpleItem this[int position]
{
get { return _mySimpleItemLoader.MySimpleItems[position]; }
}

Pretty standard. These methods just provide you with places you can do additional work, if you need to.

In our case the only ones we’re really interested in are the Count property, which tells the adapter how many items there are currently in the list. I guess it helps the adapter allocate memory, set the correct proportions on the scroll bars, stuff like that. This guy gets invoked every time we fire a NotifyDataSetChanged at the data adapter, to say ‘hey, you’ve got more items in your data thingy!’.

The MySimpleItem this is just a self-referencing property which returns the MySimpleItem at position in the list.

Now, coming back to the GetView method we’ve overridden. This is the core of the adapter and handles the linking of items in the MySimpleItemLoader.MySimpleItems with the actual grid view display items. For the sake of brevity (and clarity), I’ve left out some of the performance-enhancing bits. I’ll cover those in another post. But for what we need, this will suffice:

public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = _mySimpleItemLoader.MySimpleItems[position];

View itemView = convertView ?? LayoutInflater.From(_context).Inflate(Resource.Layout.MyGridViewCell, parent, false);
var tvDisplayName = itemView.FindViewById<TextView>(Resource.Id.tvDisplayName);
var imgThumbail = itemView.FindViewById<ImageView>(Resource.Id.imgThumbnail);

imgThumbail.SetScaleType(ImageView.ScaleType.CenterCrop);
imgThumbail.SetPadding(8, 8, 8, 8);

tvDisplayName.Text = item.DisplayName;
imgThumbail.SetImageResource(Resource.Drawable.Icon);

return itemView;
}

It’s pretty straightforward:

  • It gets the current MySimpleItem at position.
  • It checks to see if the currentView is null. currentView is an instance of a grid view item which may have been used before, and been recycled. If it hasn’t been recycled, it’ll be null, and we need to make a new one.
  • If we need to make a new one, then here’s where we use our application Context to inflate the simple xml which describes our individual grid view item ‘cell’. In our case, it’s just an ImageView for a thumbnail, and a TextView for the MySimpleItem.DisplayName text.
  • If the currentView is not null, it means it’s come out of the recycling bucket, and we can just reuse it.

For more on the ‘Recycling’, check out the References section at the end of this post.

Either way, we now have a grid view item. The next step is just to locate the image and text view controls on it (using FindViewById), and set the relevant property on each to the value that’s come from the item variable.

  tvDisplayName.Text = item.DisplayName;
imgThumbail.SetImageResource(Resource.Drawable.Icon);

As I mentioned before, there’s ways to speed this operation up. I’ve omitted them for now.

At the time of writing I didn’t have a list of images, or image urls, so I’m simply setting the ImageView to the application’s icon.

With some of the attributes removed for brevity, the MyGridViewCell.xml file looks something like:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>

<ImageView
android:id="@+id/imgThumbnail"
android:layout_width="160dp"
android:layout_height="160dp"
android:padding="5dp"
>
</ImageView>

<TextView
android:id="@+id/tvDisplayName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="18sp"
>
</TextView>

</LinearLayout>

(the full source is available in this GitHub repo)

And we’re done with MyGridViewAdapter.

Displaying the Grid Items

Finally we need to make a GridView, hook it up to the MyGridViewAdapter, and monitor the GridView scrolling events to tell our MySimpleDataLoader to load more stuff.

We can do all of this in the standard MainActivity with just a few lines of code:

[Activity(Label = "GridViewInfiniteScroll", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : Activity
{
private GridView _gridView;
private MySimpleItemLoader _mySimpleItemLoader;
private MyGridViewAdapter _gridviewAdapter;

private readonly object _scrollLockObject = new object();

private const int ItemsPerPage = 24;
private const int LoadNextItemsThreshold = 6;

protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);

_mySimpleItemLoader = new MySimpleItemLoader();
_mySimpleItemLoader.LoadMoreItems(ItemsPerPage);

_gridView = FindViewById<GridView>(Resource.Id.gridView);
_gridviewAdapter = new MyGridViewAdapter(this, _mySimpleItemLoader);
_gridView.Adapter = _gridviewAdapter;
}

}

Nothing spectacular. It simply instantiates a new MySimpleItemLoader and tells it to load the first set of items, ready for immediate display. Then it hooks up a new instance of the MyGridViewAdapter with the application Context and our loader instance to the GridView.

And if you load it up at this point, you’ll get a grid view with 24 items in it.

screenshot

It’ll scroll, but when you get to the bottom, it’ll just stop. That’s fine, if you only have 24 items. But we have an infinite list of items to display!

Adding The Infinite Scroll

We need to add one more method to this activity to get it to do this, and attach it to the GridView.Scroll event. Easily done.

Add an event handler somewhere after you’ve instantiated the GridView:

protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);

_mySimpleItemLoader = new MySimpleItemLoader();
// snip...
_gridView.Scroll += KeepScrollingInfinitely;
}

private void KeepScrollingInfinitely(object sender, AbsListView.ScrollEventArgs args)
{
lock (_scrollLockObject)
{
var mustLoadMore = args.FirstVisibleItem + args.VisibleItemCount >= args.TotalItemCount - LoadNextItemsThreshold;
if (mustLoadMore && _mySimpleItemLoader.CanLoadMoreItems && !_mySimpleItemLoader.IsBusy)
{
_mySimpleItemLoader.IsBusy = true;
Log.Info(TAG, "Requested to load more items");
_mySimpleItemLoader.LoadMoreItems(ItemsPerPage);
_gridviewAdapter.NotifyDataSetChanged();
_gridView.InvalidateViews();
}
}
}

What does this do?

  • The scroll event fires MANY times per second, so we need a way to ensure that the first event-on-a-thread that comes along has exclusive access to this method. Otherwise we’ll have a massive re-entrancy problem and maybe even a stack overflow (when was the last time you saw one of those?!). So we lock it across all threads for the duration of that method.
  • There’s a quick check to see if we’ve reached the point at which we need to load more stuff. I’ve set an arbitrary threshold of 6 items from the end. We use a combination of args.FirstVisibleItem, args.VisibleItemCount, args.TotalItemCount and LoadNextItemsThreshold to figure out where we are.
  • If we have reached the point at which we do need to load more items, we go and get them.
  • Then we tell the MyGridViewAdapter that its backing data set has changed, so it can do any background maintenance work it needs to do,
  • Finally we tell the GridView to update whatever it’s currently displaying (which will also make sure the scrollbar is set to the correct proportions and position).

And if you load your items fast enough, the user won’t even notice. Obviously if you’re on a slow data connection or something else is bottlenecking your retrieval, you may need to set a higher reload threshold, so it goes earlier. Or temporarily display some sort of loading indicator so that your users don’t think they’ve reached the end of the list, or that your app has locked up.

And get your next set of items asynchronously!

That’s it. We’re done.

The source code for this example is sitting over in this GitHub repo

I initially started out with using an adapter based on the ArrayAdapter. I’m dealing with a list of items, and an array is pretty close to a list, right? Man, that was a poor choice. Nothing I did could get the ArrayAdapter’s NotifyDataSetChanged method call to actually do anything. And without that, the GridView’s call to invalidate its views did nothing either. More items would load, but none would display. I found a workaround, where I’d recreate a brand new ArrayAdapter with all the new items in it, every time, and reattach it to the GridView. That works too, but it’s really clunky, and I felt dirty doing it.

References

Some references you may find useful:

Everything in here is released as OSS under the MIT license, so feel free to use it any way you like.


comments powered by Disqus