Skip to content

ListStore Guide: Consuming a list (WIP)

Oguz Kocer edited this page Jan 22, 2020 · 2 revisions

This guide is currently WIP and its contents might change significantly while the rest of the guide is fleshed out. If you have any questions/feedback, please reach out to @oguzkocer.

Table of Contents // TODO: Add links for each page

  1. Introduction
  2. Most important component: The ListDescriptor
  3. How to consume an existing list
  4. How to implement a new list
  5. Sectioning (link added in consumption)
  6. Consider this! (Common gotchas)
  7. Internals

Please make sure to read the Introduction first as this guide builds on it.

Before going over the current API, here are a few use cases to consider:

  • Filters
  • Searching
  • Sectioning
  • Manual interference (temporarily hiding/adding rows)

These are achieved through 2 simple interfaces. The first one is ListDescriptor which has its own page and you should absolutely check it out before going on further. As explained in its page, you can use ListDescriptor for filter & search functionality.

The second interface is ListItemDataSourceInterface which is used to tie the list data with the actual item data and its getItemIdentifiers method can be used to achieve sectioning and manual interference. Sectioning has its own page which you can check it out [here](// TODO: Add link).

Here is the interface definition:

interface ListItemDataSourceInterface<LIST_DESCRIPTOR : ListDescriptor, ITEM_IDENTIFIER, LIST_ITEM>

The important bit here is that both ITEM_IDENTIFIER and LIST_ITEM are generic types. Which means you'll decide what identifier to use for each item and the model that your UI will deal with. Let's see why this matters:

/**
 * Should transform a list of remote ids for the given [LIST_DESCRIPTOR] to a list [ITEM_IDENTIFIER]s to be used by
 * [getItemsAndFetchIfNecessary]. This method allows the implementation of this interface to make the modifications
 * to the list as necessary. For example, a list could be transformed to:
 *
 * * Add a header
 * * Add an end list indicator
 * * Hide certain items
 * * Add section headers
 */
fun getItemIdentifiers(
    listDescriptor: LIST_DESCRIPTOR,
    remoteItemIds: List<RemoteId>,
    isListFullyFetched: Boolean
): List<ITEM_IDENTIFIER>

Basically, you will be given the ListDescriptor for this list, the remote ids in the DB for it and whether all its data is fetched. Then, you're required to return a list of ITEM_IDENTIFIERs of your choice. Which means, you can do all sorts of things with it as shown in its docs. Tip: Sealed classes are good candidates for ITEM_IDENTIFIER.

For example, in WPAndroid's PostListItemDataSource we use this for adding local items, sectioning search results and adding an end list indicator.

As covered in the Introduction, it's very important to remember that the actual item data might not be available at this stage and should never be relied upon. If more than the id of each item is required, such as status of a post for sectioning, it'll need to be saved in the DB as a separate table. Please see [sectioning page](//TODO: Add a link) for details.

/**
 * Should return a list [LIST_ITEM]s for the given [LIST_DESCRIPTOR] and the list [ITEM_IDENTIFIER]s that will be
 * provided by [getItemIdentifiers].
 *
 * It should also fetch the missing items if necessary.
 */
fun getItemsAndFetchIfNecessary(
    listDescriptor: LIST_DESCRIPTOR,
    itemIdentifiers: List<ITEM_IDENTIFIER>
): List<LIST_ITEM>

Here you'll get to decide what each ITEM_IDENTIFIER translates to in your UI for a given ListDescriptor. That most likely means you'll query the DB for the actual items and add placeholders for things like section headers and end list indicators. If the actual item is not yet fetched, you will need to fetch them here. Tip: Sealed classes are good candidates for LIST_ITEM.

P.S from @oguzkocer: One unfortunate thing about this method is that it has 2 jobs which in general is a pretty bad thing in my opinion. However, any other solution I explored had worse tradeoffs. Hopefully with its naming and the documentation, it's not too big of a deal.

/**
 * Should fetch the list for the given [LIST_DESCRIPTOR] and an offset.
 */
fun fetchList(listDescriptor: LIST_DESCRIPTOR, offset: Long)

This is very straightforward, you'll most likely just dispatch an action to fetch the list and let FluxC internals take care of the rest. How the fetched data makes its way to ListStore is not something you need to think about as a consumer. However, if you are implementing a new feature, you'll need to implement this. See [adding a new list page](// TODO: Add link) for details.

Now that you know what a ListDescriptor is and you implemented a data source for your list, you can simply request a PagedListWrapper with ListStore.getList:

/**
 * This is the function that'll be used to consume lists.
 *
 * @param listDescriptor Describes which list will be consumed
 * @param dataSource Describes how to take certain actions such as fetching a list for the item type [LIST_ITEM].
 * @param lifecycle The lifecycle of the client that'll be consuming this list. It's used to make sure everything
 * is cleaned up properly once the client is destroyed.
 *
 * @return A [PagedListWrapper] that provides all the necessary information to consume a list such as its data,
 * whether the first page is being fetched, whether there are any errors etc. in `LiveData` format.
 */
fun <LIST_DESCRIPTOR : ListDescriptor, ITEM_IDENTIFIER, LIST_ITEM> getList(
    listDescriptor: LIST_DESCRIPTOR,
    dataSource: ListItemDataSourceInterface<LIST_DESCRIPTOR, ITEM_IDENTIFIER, LIST_ITEM>,
    lifecycle: Lifecycle
): PagedListWrapper<LIST_ITEM>

And that's it, your list is ready to be consumed as a PagedList that's backed by a LiveData.

The PagedListWrapper usage is very straightforward and covered by its docs, but there is one thing I want to note here that's very important.

/**
 * A method to be used by clients to tell the data needs to be reloaded and recalculated since there was a change
 * to at least one of the objects in the list. In most cases this should be used for changes where the depending
 * data of an object changes, such as a change to the upload status of a post. Changes to the actual data
 * should be managed through `ListStore` and shouldn't be necessary to be handled by clients.
 */
fun invalidateData() {
    invalidate()
}

This basically means that you don't need to worry about individual data changes and refreshing just one row of a list. Your list should be backed by a DiffUtil with smart diffing, so when the UI might need to reflect a change, you'll need to call invalidateData. Since we are using PagedList, only the visible and preloaded items will need will be refreshed. You should, of course, still try and limit how many times this is called if multiple rows are updated at the same time.