visit
“, also known as paging, is the process of dividing a document into discrete pages, either electronic pages or printed pages.”It is most common technique to manage large data set at server/client side to distribute in chunks called as pages. In today’s time, Social media client apps improvised this by inventing “Infinite scroll”.Infinite scrolling allows users to load content continuously, eliminating the need for user’s explicit actions. App loads some initial data and then load the rest of the data when the user reaches the bottom of the visible content. This data is divided in pages.
Initial, I was not clear on what and how part. My refactored efforts were scattered all over the place. While I ended up having a non-intrusive solution (what I played out as a requirement), I learned an important lesson after making mistakes.
Zoom out as far as you can until you are clear who is playing what role.
Initially I was relentlessly trying to decouple/refactored code without identifying my interface and participant objects or classes. You can only do “Separation of Concern” when you identify which class is going to do what.
What it takes to use your framework/Library
When I started refactoring my code for library, I had not laid out my interfaces well. Generally this is a common problem. We usually don’t developed solutions which are being designed for larger audience but used within few use cases. It really open our eyes when we start thinking in this direction. And yes, this should be second step when we develop a framework.Most of pagination responses have the common structure which comprises of total Page count, current page and an array of elements. With this assumption, A struct can be defined for all responses over generic parameter Type.
public struct PageInfo<T> {
var types: [T]
var page: Int
var totalPageCount: Int
public init(types: [T], page: Int, totalPageCount: Int) {
self.types = types
self.page = page
self.totalPageCount = totalPageCount
}
}
public protocol PageableService: class {
/// Indicates when to load next page
func load(pageInteractor: PageInteractor, page: Int) -> Void
/// Cancel request if required
func cancelAllRequests()
}
Function LoadPage is called by PageInteractor when it figure out that there is more data to load. A typical implementation can be
func load(pageInteractor: PageInteractor, page: Int) -> Void {
guard let resource: Resourse<PagedResponse<[User]>> = try? prepareResource(page: page,
pageSize: pageSize,
pathForREST: "/api/users")
else { return }
// Construct PageInfo to be returned
var info: PageInfo<User>?
networkManager.fetch(res: resource) { (res) in
switch res {
case let .success(result):
info = PageInfo(types: result.types,
page: result.page,
totalPageCount: result.totalPageCount)
case let .failure(err):
print(err)
}
// Require to call method on PageInteractor for updating result.
pgInteractor.returnedResponse(info)
}
}
First method func loadPage(_ page: Int) -> Void indicates when to request which page. As It loads next set of data, the loaded data needs to be sent to PageInteractor for updating result.
Now calling PageInteractor, is a requirement which can be missed as API is not enforcing it.
Completion block helps in situations like this. So we can update an interface with completion block. As PageInfo<T> operates on generic parameter, This requires to create a generic interface to adopt by.
public protocol PageableService: class {
func loadPage<Item: Decodable>(_ page: Int,
completion: @escaping (PageInfo<Item>?) -> Void)
func cancelAllRequests()
}
Pageable gives a feature where duplicate entries from server can be avoided at client side if somehow new entries are being added dynamically at server end. More can be found .
To use this feature your pagination data should have a unique Id in response array so that duplicates can be identified. To make this feature usable, It requires a user to give a power to provide a generic way to identify that field.Earlier due to inability to provide this information to PageInteractor, User needs to implement interface PageDataSource methods which checks items in dictionary and then add in final array to avoid duplication. This is very redundant task for user and ideally handled by PageInteractor only.
extension UserView: PageDataSource {
func addUniqueItems(for items: [AnyObject]) -> Range<Int> {
let startIndex = pgInteractor.count()
if let items = items as? [User] {
for new in items {
if pgInteractor.dict[String(new.id)] == nil {
pgInteractor.dict[String(new.id)] = String(new.id)
pgInteractor.array.append(new) // adding items in array to be displayed
}
}
}
return startIndex..<pgInteractor.count()
}
func addAll(items: [AnyObject]) {
if let items = items as? [User] {
pgInteractor.array = items
for new in items {
pgInteractor.dict[String(new.id)] = String(new.id)
}
}
}
}
KeyPath are a way to reference properties without invoking them. It can be composed to make different path and used to get/set underlying properties.
With the KeyPath passed in the initialiser of PageInteractor, implementation of both methods can move entirely in PageInteractor. This will relive user from not implementing PageDataSource.
extension PageInteractor {
/**
Server can add/remove items dynamically so it might be a case that
an item which appears in previous request can come again due to
certain element below got removed. This could result as duplicate items
appearing in the list. To mitigate it, we would be creating a parallel dictionary
which can be checked for duplicate items
- Parameter items: items to be added
- Parameter keypath: In case if duplicate entries has to be filter out,
It requires keypath of unique items in model data.
*/
open func addUniqueFrom(items: [Element], keypath: KeyPath<Element, KeyType>?) -> Range<Int> {
let startIndex = count()
if let keypath = keypath {
for new in items {
let key = new[keyPath: keypath]
if dict[key] == nil {
dict[key] = key
array.append(new)
}
}
}
return startIndex..<count()
}
/** Add all items, If there is empty list in table view
- Parameter items: items to be added
- Parameter keypath: In case if duplicate entries has to be filter out,
It requires keypath of unique items in model data.
*/
open func addAll(items: [Element], keypath: KeyPath<Element, KeyType>?) {
array = items
guard let keypath = keypath else {
return
}
for new in items {
let key = new[keyPath: keypath]
dict[key] = key
}
}
}
With default implementation embedded in PageInteractor, All it takes is to provide KeyPath of unique data type to PageInteractor to use this feature.
let pageInteractor: PageInteractor<UserModel, Int> = PageInteractor(firstPage: firstReqIndex, service: service, keyPath: \UserModel.id)
While developing infinite scrolling as a framework, was fun and turned out to be an easy solution to integrate with few steps to follow. I learned an important lesson on what part. There is always a room of improvement when you look back to your solution. I did a few iterations while finalising interface for Pageable and now I feel next time I can avoid few of those.
You can find library at Github, feel free to give it a try/feedback !