Adding Headings to a CursorAdapter
A recent StackOverflow question
re-raised an oft-cited challenge:
adding headings to a
CursorAdapter. My examples of headings
in lists use
which is suitable for where each
individual section can be represented by its own
CursorAdapter on a distinct
Cursor). In some cases,
that’s not a great approach — doing a single query and
having a single
Cursor would be superior. However, then
introducing the headings becomes a bit more of a challenge.
Eventually, I’ll probably run into a case where I have this itch and will scratch it. At that point, I’ll poke at the Android Open Source Project for places where it uses this technique (e.g., contacts) and see how they approach it. Then, after failing to understand most of the code I read, I’ll cook up something.
Since I don’t really have that itch to scratch right now, but since this issue comes up, here’s my shoot-from-the-hip way I’d approach the problem:
As the “for instance” data model, let’s imagine a database
of books and categories. Each book is in one category, to
keep this story simple. We want a
ListView of all books,
sorted by category, with headings for the categories.
Our query would use a join to bring back the category title,
along with the relevant book data (e.g., book title). We
would also do an
ORDER BY to sort our data by category, then
by whatever the order is to be within each heading (e.g.,
by book title).
After doing the query and getting the
Cursor, I would spin
Cursor and find out the “breaks” when we switch
from one category to the next. You could do this with a separate
query, and that might be faster, though you would have to feel
comfortable that your data would not change between those two
queries. I’d have to research SQLite’s transaction semantics
to determine if a transaction will perform enough locking to
cover this case.
ArrayList of break information, I would create an
WrapperListAdapter that would be my decorating adapter
supplying the headings, using a regular
Cursor for the books. Let’s call this decorating adapter
BookCategoryAdapter for the sake of this post.
BookCategoryAdapter would simply delegate most
behaviors to the underlying
CursorAdapter. Notable overrides
getCount()would return the sum of
CursorAdapterand the number of headings, determined from the number of breaks.
getViewTypeCount(), presumably returning
2, or perhaps
CursorAdapterplus one for the headings.
getItemViewType(), which would use a private
iCanHazHeading()method to determine if the
positionrepresents a heading or not. If it does, it would return the view type for the heading; if not, it would return a view type for the book.
iCanHazHeading()would make its determination based on the breaks.
getView()would also use
iCanHazHeading()to determine if the
positionis a heading, and return the heading
Viewif so. The heading
Viewwould be populated based on the category column in the
Cursor. Otherwise, for regular rows, it would delegate to the wrapped adapter, modifying
positionto take into account any headings that appear before this row.
The StackOverflow question in, er, question indicated that
performance was an issue. The primary point of overhead
in this implentation is determining where the category breaks are.
However, that should really only be slow for massive lists,
and the solution to that is simple: don’t use massive lists.
I know there are some developers who think that 10,000-item
ListViews are awesome. I find them atrocious, whether in
a desktop, Web, or mobile application. If your data is potentially
unbounded like this, you need a better UI model that lets
the user deal with a smaller list (e.g., list of categories,
each leading to a list of books in that category). Expecting
the user to sift through 10,000 entries on a 3” smartphone
screen is bad, and adding headings will not help much.
Learn second-generation Android app development — with Kotlin and the Android Jetpack — through CommonsWare’s Android app development training!