TodoList App – Part 5, Using Android Paging Library

Reading Time: 3 minutes

In this article I’ll talk about Android Paging 2 library. The paging library has evolved quite a lot however when I began to use it for my project I had started with Paging 2 library however now that Paging 3 library is out this might not be relevant. The article describes some of my challenges when working directly with Paging 2 library.

Problem Description

So what I wanted to do was fetch a bunch of data from a REST API server and then push that data out to RecyclerView. The REST API already supports offset and limit based results so now I had to just call the correct offset with a correct limit to fetch data from web server.

Paging 2 library provided three different types of solution, basically data sources. These are

  • PageKeyedDataSource
  • ItemKeyedDataSource
  • PositionalDataSource

What I wanted to do was load my data directly off network without using an intermediate database. 

Positional DataSource can’t do this since it tries to load everything at one go, this is suitable for something like database + network since once you’ve data in database you can use other data sources.

ItemKeyedDataSource

I ended up using this data source when I was trying to use scrolling with RecyclerView and fetching data based on that. The following code shows the ItemKeyedDataSource class used for this purpose

public class TodoItemKeyedDataSource extends PageKeyedDataSource<TodoItemKey, TodoItem> {
    private final TodoItemNetworkDataSource dataSource;
    private Map<String, String> apiHeaders;

    private int PAGE_SIZE = 10;
    public TodoItemKeyedDataSource(@NonNull TodoItemNetworkDataSource dataSource) {
        this.dataSource = dataSource;
    }
    public void setPageSize(int pageSize) {
        if (pageSize > 0)
            this.PAGE_SIZE = pageSize;
    }


    public void setApiHeaders(Map<String, String> apiHeaders) {
        this.apiHeaders = apiHeaders;
    }

    private List<TodoItem> doFetchItemsFromNetwork(int offset, int count, boolean shared) {
        if (offset < 0)
            offset = 0;
        Result result = dataSource.getTodoItems(offset, count, shared, this.apiHeaders);
        if (result != null) {
            if (result instanceof Result.Success) {
                Response response = (Response)
                        ((Result.Success) result).getData();
                if (((Double) response.extras.get("count")).intValue() == 0) {
                    return null;
                }
                ArrayList<TodoItem> itemList = (ArrayList<TodoItem>) response.extras.get("items");
                return itemList;
            }
        }
        return null;
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<TodoItemKey> params, @NonNull LoadInitialCallback<TodoItemKey, TodoItem> callback) {
        int fetchedItemsCount = 0;
        List<TodoItem> list = doFetchItemsFromNetwork(0, params.requestedLoadSize, true);
        if (list != null)
            fetchedItemsCount = list.size();
        callback.onResult(list, 0, fetchedItemsCount, null, new TodoItemKey(0).getNextKey());
    }

    @Override
    public void loadBefore(@NonNull LoadParams<TodoItemKey> params, @NonNull LoadCallback<TodoItemKey, TodoItem> callback) {
        int offset = 0;
        int fetchedItemsCount = 0;
        offset = params.key.getPgIdx() * PAGE_SIZE;

        List<TodoItem> list = doFetchItemsFromNetwork(offset, params.requestedLoadSize, true);
        if (list != null)
            fetchedItemsCount = list.size();
        else
            list = new ArrayList<TodoItem>(0);
        callback.onResult(list, params.key.getPrevKey());
    }

    @Override
    public void loadAfter(@NonNull LoadParams<TodoItemKey> params, @NonNull LoadCallback<TodoItemKey, TodoItem> callback) {
        int offset = 0;
        int fetchedItemsCount = 0;
        offset = params.key.getPgIdx() * PAGE_SIZE;

        List<TodoItem> list = doFetchItemsFromNetwork(offset, params.requestedLoadSize, true);
        if (list != null)
            fetchedItemsCount = list.size();
        else
            list = new ArrayList<TodoItem>(0);
        callback.onResult(list, params.key.getNextKey());
    }
} 
 

In the above code, the most important part is how to tell when there’s no more data. Note that the keys are simply page indices, you can put anything here but it should allow you to load that particular page of data when required, as the user scrolls through the list back and forth.

To notify that there’s no more data available from network we create a ZERO size list and use that in callback.Result.

Building PagedList

In order to create PagedList we just use LivePagedListBuilder. You don’t have to override setCallBack with your own with this data source. The following snippet shows the config and how the PagedList is created eventually.

PagedList.Config config = new PagedList.Config.Builder()
                .setPageSize(PAGE_SIZE)
                .setEnablePlaceholders(true)
                .setPrefetchDistance(2*PAGE_SIZE)
                .build();
todoItemListData = new LivePagedListBuilder<>(todoItemDataFactory, config).build();
 

Invalidating DataSource

It’s possible that your data might’ve changed at server and you would like to load it afresh. In which case it’s better to invalidate this datasource and create a new one which’ll basically do the same things.

You might want to do this invalidation in some external event, viz swipe to refresh and load data again from server. It’s possible that you’re somewhere in the middle of your data set when user does this so it might help to know where you are so that you can send off the current offset to your REST API server. 

Leave a Reply