There are plenty of articles out there talking about the amazing Android Architecture Components, how we can combine them in an MVVM architecture and make them work as a charm. From my point of view, that's true, the Android Architecture Components are awesome!
There's also a ton of articles talking about the new Android Paging Library and how we can combine it with Room Persistence Library and LiveData to make paging as easy as possible. I suppose that you are already familiar with the topic :)
So I don't want to write about the new Android Components or how we should use them. Today I want to tell you how we can integrate a numerated paged service, to the best of my knowledge, using our new Fountain library. A numerated paged service is an endpoint which returns a list of entities structured in pages with sequential page numbers.
Why not use the Android Paging Library directly?
To be able to integrate these kind of services you'll have to write a lot of code to obtain the necessary paging component, LiveData<PagedList<T>>
.
Once you have it, the paging issue becomes trivial, but getting it is not. And this is only if you don't need database support, in that case you'll have to write a lot more code.
This is where Fountain comes to life.
It provides everything you'll need to work with these services and more, easily and without boilerplate.
To read this post, you should already know the Repository Architectural Pattern and the basics of these libraries:
Let's get down to the nitty-gritty stuff!
First I want to tell you about a cool idea from Google, that they are using in some of their example projects to handle all services that return a list.
They believe that you can handle all list streams with a Listing
component, which contains basically five elements:
data class Listing<T>(
val pagedList: LiveData<PagedList<T>>,
val networkState: LiveData<NetworkState>,
val refresh: () -> Unit,
val refreshState: LiveData<NetworkState>,
val retry: () -> Unit
)
T
represented as a LiveData
of a PagedList
.The network state is represented as:
enum class Status {
RUNNING,
SUCCESS,
FAILED
}
data class NetworkState private constructor(
val status: Status,
val throwable: Throwable? = null) {
companion object {
val LOADED = NetworkState(Status.SUCCESS)
val LOADING = NetworkState(Status.RUNNING)
fun error(throwable: Throwable?) = NetworkState(Status.FAILED, throwable)
}
}
Suppose that you are following the Repository Pattern and you want to expose a paged entity list from a service source.
In that case, your repository should implement a method which returns a Listing
of that entities.
I'll give you an example.
Suppose that you have an app which lists the GitHub users whose usernames contain a specific word.
So, if you use Retrofit and RxJava, you can define the service call as:
data class GhListResponse<T>(
val total_count: Long,
val items: List<T>
)
data class User(
var id: Long,
var login: String?,
var avatarUrl: String?
)
@GET("/search/users")
fun searchUsers(@Query("q") name: String,
@Query("page") page: Int,
@Query("per_page") pageSize: Int
): Single<GhListResponse<User>>
This service is pretty similar to most paged services I've seen, so the big question here is how we can integrate this service in a repository using the new Paging Component.
Furthermore, the question could be how we could convert the Single<GhListResponse<User>>
response to a Listing<User>
structure.
The first thing that we should consider, is deciding if we need a database source to cache the data. It could seem easy, but it's not. Some people could say that if we want to search entities by a key, a database cache isn't the best option because the response may change constantly and frequently, and at the same time the app user doesn't repeat the same search query multiple times. However, saving data in a database source has some advantages. For example, you can make your app work offline, make less use of the backend, hide network problems, manage data better and share entities easier.
Personally, I prefer taking the cache approach, but I know that it depends on the problem. In this post we will explore both strategies using Fountain.
Fountain is an Android Kotlin library conceived to make your life easier when dealing with paged endpoint services, where the paging is based on incremental page numbers (e.g. 1, 2, 3, ...).
The features are:
Listing
structure based on a Retrofit and RxJava service.Listing
structure with cache support using Retrofit and RxJava for the service layer and a DataSource
for caching the data. In the examples we will use Room Persistence Library to provide the DataSource
but you could use any other DataSource
.In this first part of the series we'll see how we can integrate the first functionality. The second one will be explained in the next part.
To integrate Fountain into your app, you have to include the following dependency in your gradle app.
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
implementation 'com.github.xmartlabs:fountain:0.2.+'
}
The library defines two structures to handle the network requests.
The PageFetcher
is used to fetch each page from the service, whereas the NetworkDataSourceAdapter
will be used to handle the paging state.
interface PageFetcher<T> {
@CheckResult
fun fetchPage(page: Int, pageSize: Int): Single<out T>
}
interface NetworkDataSourceAdapter<T> : PageFetcher<T> {
@CheckResult
fun canFetch(page: Int, pageSize: Int): Boolean
}
The paging state will be managed by the library using mainly two methods:
Single<out T>
, where T
can be anything.canFetch(page = 5, pageSize = 10)
then you should return false
.
You have to implement this function using the service specification.
Sometimes the service returns the page amount or the entity amount in the response headers or in the response body, so you'll have to use that information to implement this function.
However, if you know exactly the page or entity count, the library provides a way to make this work easier.
I will show that later.To use the Fountain Network support, you just have to implement a NetworkDataSourceAdapter<ListResponse<T>>
, where the ListResponse<T>
is how the library expects the service response.
interface ListResponse<T> {
fun getElements(): List<T>
}
So, following the example, our paging handler would be:
data class GhListResponse<T>(
val total_count: Long,
val items: List<T>
) : ListResponse<T> {
override fun getElements() = items
}
val networkDataSourceAdapter =
(object : NetworkDataSourceAdapter<ListResponse<User>> {
override fun canFetch(page: Int, pageSize: Int) = true
override fun fetchPage(page: Int, pageSize: Int) =
userService.searchUsers(userName, page, pageSize)
})
It's not so hard, right?
However, the example has a problem: the canFetch
method is returning true
for all invocations, so we have implemented an endless paging solution.
In most cases the solution will not be so useful, so let's fix it.
If we take a look at the GitHub response, we can see that GitHub is returning the amount of entities in each response.
That is great, we can use that information to implement a real canFetch
method.
Remember that Fountain provides a way to achieve it automatically.
To do that, the library defines two response interfaces and two NetworkDataSourceAdapter
providers:
interface ListResponseWithEntityCount<T> : ListResponse<T> {
fun getEntityCount() : Long
}
interface ListResponseWithPageCount<T> : ListResponse<T> {
fun getPageCount(): Long
}
class NetworkDataSourceWithTotalEntityCountAdapter<T>(
val pageFetcher: PageFetcher<out ListResponseWithEntityCount<T>>,
firstPage: Int = 1
) : NetworkDataSourceAdapter<ListResponse<T>>
class NetworkDataSourceWithTotalPageCountAdapter<T>(
val pageFetcher: PageFetcher<out ListResponseWithPageCount<T>>,
firstPage: Int = 1
) : NetworkDataSourceAdapter<ListResponse<T>>
Depending on whether we know the entity or the page count, we will use either NetworkDataSourceWithTotalEntityCountAdapter<T>
or NetworkDataSourceWithTotalPageCountAdapter<T>
.
Given that the GitHub response has the amount of entities, we have to update the GhListResponse<T>
to implement NetworkDataSourceWithTotalEntityCountAdapter<T>
data class GhListResponse<T>(
val total_count: Long,
val items: List<T>)
: ListResponseWithEntityCount<T> {
override fun getEntityCount() = total_count
override fun getElements() = items
}
Then, what's left to create the NetworkDataSourceAdapter
is to build a PageFetcher<GhListResponse<T>>
.
val pageFetcher = (object : PageFetcher<GhListResponse<User>> {
override fun fetchPage(page: Int, pageSize: Int) =
userService.searchUsers(userName, page, pageSize)
})
val networkDataSourceAdapter =
NetworkDataSourceWithTotalEntityCountAdapter(pageFetcher)
Recall that at the beginning I said that the only required thing to create a Listing
structure using the Fountain Network support was a NetworkDataSourceAdapter
.
So we can invoke the listing creator this way:
Fountain.createNetworkListing(networkDataSourceAdapter)
And that's all, we have our Listing<User>
structure!
In addition, there are some optional parameters that you can define when you are constructing the Listing
object:
firstPage: Int
: The initial page number, by default its value is 1.ioServiceExecutor : Executor
: The executor with which the service call will be made. By default, the library will use a pool of 5 threads.pagedListConfig: PagedList.Config
: The paged list configuration.
In this object you can specify several options, for example the pageSize
and the initialPageSize
.In the next part of this series we'll see how we could get a Listing
component which uses a DataSource
cache to store the data.
Example code can be found in the sample app.
The Listing
component is the key of the library.
It provides an awesome way of displaying the paged entity list and reflecting the network state in the UI.
If you have this component implemented, you can create an MVVM architecture app and combine it with the repository pattern.
If you get a repository which provides a Listing
component of each paged list, you will be creating a robuster app.
Fountain provides a way to create a Listing
component easily for a common paged service type, which are the services where the paginated strategy is based on an incremental page number.
It also provides two ways to go: a mode with network support and a mode with network + cache support. The strategy you choose will depend on your problem.
I suggest you give it a shot. We'll be glad if you provide feedback :)