Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions example/lib/repositories/posts_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@ class PostsRepository {
// call api and get the response, and pass the contents
// in the constructor for [PaginatedItemsResponse].

// Don't forget to pass the idGetter parameter here.
final res = await Dio().get('https://jsonplaceholder.typicode.com/posts');
return PaginatedItemsResponse<Post>(
listItems: res.data?.map((e) => Post.fromJson(e)).cast<Post>(),
// no support for pagination for current api
paginationKey: null,
return PaginatedItemsResponse<Post>.fromListWithNoPaginationSupport(
data: res.data.map((e) => Post.fromJson(e)).cast<Post>().toList(),
idGetter: (post) => post.id.toString(),
);
}
Expand Down
266 changes: 204 additions & 62 deletions lib/src/models/paginated_items_response.dart
Original file line number Diff line number Diff line change
@@ -1,106 +1,248 @@
import 'dart:developer' as dev;

/// The response object that carries the list of items and handles pagination
/// internally. The [paginationKey] is optional and can be of any type.
/// If not passed, it is assumed that the API does not support pagination.
///
/// The [idGetter] should be passed when receiving the response from the API, it
/// is required for functions like `updateItem`, `findByUid`
/// and avoiding duplication of items in list (compares id).
extension PaginationModelExtension<T> on PaginatedItemsResponse<T>? {
// do not call this like response?.update, call it like response.update...
PaginatedItemsResponse<T> update(
PaginatedItemsResponse<T> newResponse, {
bool reset = false,
}) {
return reset || this == null ? newResponse : this!._update(newResponse);
}
}

class PaginatedItemsResponse<T> {
/// constructor
dynamic paginationKey;
List<T> data;
int itemsPerPage;
final String Function(T) idGetter;
final dynamic Function(T) _paginationKeyGetter;

List<T> get items => data;

dynamic getPaginationKeyForItem(T item) => _paginationKeyGetter(item);

dynamic getPaginationKeyForItemAtIndex(int index) =>
getPaginationKeyForItem(data[index]);

static bool _calculateHasMore({
required int dataLength,
required int itemsPerPage,
}) {
return dataLength >= itemsPerPage;
}

late bool _hasMore;

PaginatedItemsResponse.empty({
required this.idGetter,
}) : paginationKey = null,
_hasMore = false,
itemsPerPage = 0,
data = const [],
_paginationKeyGetter = ((_) => null);

PaginatedItemsResponse.fromListWithNoPaginationSupport({
required this.data,
required this.idGetter,
}) : paginationKey = null,
_hasMore = false,
itemsPerPage = 0,
_paginationKeyGetter = ((_) => null);

PaginatedItemsResponse({
String Function(T)? idGetter,
Iterable<T>? listItems,
dynamic paginationKey,
required this.data,
required this.itemsPerPage,
required dynamic Function(T) paginationKeyGetter,
required this.idGetter,
dynamic defaultPaginationKey,
}) : _hasMore = _calculateHasMore(
dataLength: data.length,
itemsPerPage: itemsPerPage,
),
_paginationKeyGetter = paginationKeyGetter,
paginationKey = defaultPaginationKey ??
_getPaginationKey(
data,
paginationKeyGetter: paginationKeyGetter,
);

bool get hasMore => _hasMore;

bool get isEmpty => data.isEmpty;

bool get isNotEmpty => data.isNotEmpty;

int get length => data.length;

PaginatedItemsResponse<N> updateListWithNewType<N>(
N Function(T) mapper, {
required dynamic Function(N) paginationKeyGetter,
required String Function(N) idGetter,
}) {
_idGetterFn ??= idGetter;
_update(listItems, paginationKey);
return PaginatedItemsResponse<N>(
data: data.map(mapper).toList().cast<N>(),
itemsPerPage: itemsPerPage,
idGetter: idGetter,
paginationKeyGetter: paginationKeyGetter,
defaultPaginationKey: paginationKey,
).._hasMore = _hasMore;
}

/// List of items of type [T]
List<T>? items;
void addAll(
Iterable<T> newData, {
bool shouldUpdatePaginationKey = true,
}) {
data = <T>[...data];

/// The pagination key. Can be null.
dynamic paginationKey;
final existingIds = data.map(idGetter).toSet();

/// ID getter for the object of type [T].
String Function(T)? _idGetterFn;
data.addAll(
newData.where((e) => !existingIds.contains(idGetter(e))),
);

/// If pagination supported, check if there is more data that can be loaded.
bool get hasMoreData => paginationKey != null;
if (shouldUpdatePaginationKey) updatePaginationKey();
}

/// True if [items] is not null.
bool get hasData => items != null;
PaginatedItemsResponse<T> _update(PaginatedItemsResponse<T> newResponse) {
itemsPerPage = newResponse.itemsPerPage;

/// Find an object by [id].
// ignore: body_might_complete_normally_nullable
T? findByUid(String id) {
final idx = items?.indexWhere((e) => id == _idGetterFn!(e));
if (idx != null && idx != -1) return items?[idx];
_hasMore = _calculateHasMore(
dataLength: newResponse.length,
itemsPerPage: itemsPerPage,
);

if (newResponse.isNotEmpty) {
addAll(newResponse.data, shouldUpdatePaginationKey: false);
paginationKey = newResponse.paginationKey;
}

return this;
}

T? operator [](int index) => items?[index];
void insert(
T item, {
int insertionIdx = 0,
bool removeLastIfExceedsItemsPerPage = true,
bool shouldUpdatePaginationKey = true,
}) {
data = <T>[...data];
data.insert(insertionIdx, item);

void operator []=(int index, T value) => items?[index] = value;
if (removeLastIfExceedsItemsPerPage && data.length > itemsPerPage) {
data.removeLast();
}

/// update a specific item with uid, or add if does not exists according to
/// [addIfDoesNotExist].
///
/// If [item] is null, then the item is removed.
void updateItem(
String itemUid,
T? item, {
bool addIfDoesNotExist = false,
if (shouldUpdatePaginationKey) updatePaginationKey();
}

void updatePaginationKey() {
paginationKey = _getPaginationKey(
data,
paginationKeyGetter: _paginationKeyGetter,
);
}

static dynamic _getPaginationKey<T>(
List<T> data, {
required dynamic Function(T) paginationKeyGetter,
}) {
final idx = items!.indexWhere((e) => itemUid == _idGetterFn!(e));
if (idx != -1) {
if (item == null) {
items!.removeAt(idx);
} else {
items![idx] = item;
}
return data.isEmpty ? null : paginationKeyGetter(data.last);
}

PaginatedItemsResponse<T> map(
T Function(T) mapper, {
bool inPlace = false,
bool shouldUpdatePaginationKey = true,
}) {
if (inPlace) {
data = data.map(mapper).toList().cast<T>();
if (shouldUpdatePaginationKey) updatePaginationKey();
return this;
} else {
if (item != null && addIfDoesNotExist) items!.add(item);
return PaginatedItemsResponse<T>(
data: data.map(mapper).toList().cast<T>(),
itemsPerPage: itemsPerPage,
idGetter: idGetter,
defaultPaginationKey: paginationKey,
paginationKeyGetter: _paginationKeyGetter,
).._hasMore = _hasMore;
}
}

/// Updates the response
void update(PaginatedItemsResponse<T> res) {
_update(res.items, res.paginationKey);
PaginatedItemsResponse<T> filter(
bool Function(T) predicate, {
bool inPlace = false,
bool shouldUpdatePaginationKey = true,
}) {
if (inPlace) {
data = data.where(predicate).toList().cast<T>();
if (shouldUpdatePaginationKey) updatePaginationKey();
return this;
} else {
return PaginatedItemsResponse<T>(
data: data.where(predicate).toList().cast<T>(),
itemsPerPage: itemsPerPage,
idGetter: idGetter,
defaultPaginationKey: paginationKey,
paginationKeyGetter: _paginationKeyGetter,
).._hasMore = _hasMore;
}
}

/// Append items to the list, after a successful fetch from the API.
void _update(Iterable<T>? listItems, dynamic key) {
items ??= [];
if (listItems != null) {
for (final item in listItems) {
updateItem(_idGetterFn!(item), item, addIfDoesNotExist: true);
}
bool updateItemWithId(
String id, {
required T Function(T?) updater,
bool addIfDoesNotExist = false,
bool shouldUpdatePaginationKey = true,
}) {
bool didUpdate = false;

final index = data.indexWhere((e) => idGetter(e) == id);
if (addIfDoesNotExist && index == -1) {
data.add(updater(null));
didUpdate = true;
} else {
data[index] = updater(data[index]);
didUpdate = true;
}
paginationKey = key;

if (didUpdate && shouldUpdatePaginationKey) updatePaginationKey();

return didUpdate;
}

T? findById(String id) {
final idx = data.indexWhere((e) => id == idGetter(e));
if (idx != -1) return data[idx];

return null;
}

/// Logs the response.
void log() => dev.log('\n${toString()}', name: 'PaginatedItemsResponse<$T>');

@override
String toString() {
final itemsArrString = items
?.map((item) => item.toString())
final itemsArrString = data
.map((item) => item.toString())
.map((itemName) => '\t\t$itemName,')
.join('\n');

return """
PaginatedItemsResponse<$T>({
items: ${items == null ? 'null' : '[\n$itemsArrString\n\t],'}
items: [\n$itemsArrString\n\t],
paginationKey: $paginationKey,
});""";
}

/// Clear the contents.
void clear() {
items = null;
data = [];
itemsPerPage = 0;
_hasMore = false;
paginationKey = null;
}

void operator []=(int index, T value) => data[index] = value;

T operator [](int index) => data[index];
}
Loading