Tables and Data Sources

One of the many things that annoy the crap out of me when programming iOS is feeding UITableView with data. Android makes things a little better with its ListView or rather the ListAdapter class, but that includes its own set of problems. This post, then, is an attempt to formulate an interface that is saner than either.

The Problem(s)

On the face of it, UITableView is incredibly easy to use. You have an array of stuff. You implement a class that contains that array somehow. Whenever UITableView needs to figure out what data it should display, it requests that array from your class, which it calls a data source. Fair enough. Or is it?

  1. This approach assumes that your data is always homogenous, that is, each cell in your list contains the same type of data as every other cell. Realistically, on a screen the size of a post-it note, you won’t want to do that — at least not all that often.
  2. The approach also assumes that you always have the full data available, i.e. you can’t paginate. And by paginating I don’t mean how the data is displayed visually, but how the data is fetched from a probably persistent data store. Which could be a web API, so really, really slow.

Cool Stuff

One thing that’s pretty cool about the split between the view and where the data comes from is that your view can be relatively smart with regards to how many child views it must manage. If a cell is e.g. 20 pixels tall, and the list view itself is 100 pixels tall, then it needs to at most manage (100 / 20) + 1 = 6 child views. The extra view is because the first and last cells might be partially visible.

This is good. We want to keep that.

Enter the Android

Android improves on the situation somewhat by making its ListAdapter a little smarter than UITableViewDataSource — but not much. It basically fixes the first of the above problems, by providing an API that lets ListView ask ListAdapter what view type to use for each cell.

A view type is really just an integer you define. ListView then caches child views for each type it encounters, and creates new views only when all the currently cached ones are in use, thus growing its cache lazily.

This is better. We want that, instead of what UITableView does.

Android also provides the notion of an “empty view”, that is, what to display in the ListView when there’s no data to display. That could be an error message, or a loading animation if you grab your data off the web.

This is also cool, but has the major drawback that there’s no way to differentiate (easily) between the two examples given above. That is, it can be hard to provide good feedback to the user as to why a view contains no data. We’d like to fix that.

There’s an additional level of complexity here in that the above mentioned pagination might be important. If so, apps often display a loading animation as the last entry in the list, and then append data when fresh data arrives. That is currently hard to do both in iOS as well as Android. We’d like to fix that, too.

Pagination

The thing is, the problem with slowly loading data is exactly the same as the pagination problem, if you view each request to the data source as a request for elements N..M. In the simple case where you want to load all data in one giant request, you’d have N == 0 and M == max. In the pagination case, your N and M vary more.

The current APIs on both systems have the view request an element with a given index from the data source. That’s great, but we can (somewhat) improve on that by simply changing that API to provide a slice of elements. So instead of this:

for (int x = N ; x < M ; ++x) {
  data_for_cell = source.getData(x);
}

do this:

data_for_cells = source.getData(N, M);
// or possibly
data_for_cells = source.getData(N, amount); // amount could simply be M-N

What does this buy us?

For a fast data source, nothing much. You might save a few function calls. If your data source needs to make a network request, however, the savings are potentially massive. And in a similar vein, you can return an array of view types rather than a single integer.

There is, however, a problem with this approach if you use different view types that may have different heights. If view heights are constant, given an offset N and some knowledge about the height of each cell view, it’s easy to calculate M. That is hard or impossible for cell views of varying height.

So how would you deal with that?