I am trying to implement pagination in my application but I have not been successful in doing so.
I am using Firebase, specifically Firestore with the BLOC pattern alongside Built Value which I started using recently to make pagination easier.
I would really appreciate any help or referral links how to use these technologies together.
My application architecture is as follows:
I have tried to keep to the BLOC pattern as much as possible but this in turn has made it really difficult to paginate largely ,because of using built value as built value make it really difficult to use Streams and Futures. I have looked all over the Internet but I could not find any tutorial or docs to use built value with Firestore and BLOC specifically to paginate.
The problem is that when I do any of the CRUD functions for example delete an Category from a list, the Stream Builder is not updating the list despite the pagination and everything else working.
Currently I have tried using the a Listview builder by itself which obviously didn't work at all,so I moved to a Stream Builder and tryed both Streams and Futures(.asStream) but it is not updating.
Below is some of the code:
The model:
abstract class CategoryCard
implements Built<CategoryCard, CategoryCardBuilder> {
String get category;
String get icon;
double get budget;
double get spent;
String get categoryRef;
DocumentSnapshot get document;
CategoryCard._();
factory CategoryCard([updates(CategoryCardBuilder b)]) = _$CategoryCard;
static Serializer<CategoryCard> get serializer => _$categoryCardSerializer;
}
The query:
Future<Stream<fs.QuerySnapshot>> getMoreCategoryAmounts(
fs.DocumentSnapshot documentSnapshot) async {
var user = await getCurrentUser();
print(currentMonth);
fs.Query categoryAmountQuery = _instance
.collection('users')
.document(user.uid)
.collection('amounts')
.where('year', isEqualTo: currentYear)
.where('month', isEqualTo: currentMonth)
.orderBy('category', descending: false)
.limit(7);
return documentSnapshot != null
? categoryAmountQuery.startAfterDocument(documentSnapshot).snapshots()
: categoryAmountQuery.snapshots();
}
The BLOC:
class CategoryCardBloc extends Bloc<CategoryCardEvents, CategoryCardState> {
final BPipe bPipe;
final FirebaseRepository firebaseRepository;
CategoryCardBloc({@required this.bPipe, @required this.firebaseRepository})
: assert(bPipe != null),
assert(firebaseRepository != null);
@override
CategoryCardState get initialState => CategoryCardState.intial();
@override
Stream<CategoryCardState> mapEventToState(CategoryCardEvents event) async* {
if (event is LoadCategoryCardEvent) {
yield* _mapToEventLoadCategoryCard(event);
}
}
Stream<CategoryCardState> _mapToEventLoadCategoryCard(
LoadCategoryCardEvent event) async* {
if (event.amountDocumentSnapshot == null) {
yield CategoryCardState.loading();
}
try {
Future<BuiltList<CategoryCard>> _newCategoryCards =
bPipe.getMoreCategoryCards(event.amountDocumentSnapshot);
yield CategoryCardState.loaded(
FutureMerger()
.merge<CategoryCard>(state.categoryCards, _newCategoryCards));
} on NullException catch (err) {
print('NULL_EXCEPTION');
yield CategoryCardState.failed(err.objectExceptionMessage,
state?.categoryCards ?? Stream<BuiltList<CategoryCard>>.empty());
} on NoValueException catch (_) {
print('NO VALUE EXCEPTION');
yield state.rebuild((b) => b..hasReachedEndOfDocuments = true);
} catch (err) {
print('UNKNOWN EXCEPTION');
yield CategoryCardState.failed(
err != null ? err.toString() : NullException.exceptionMessage,
state.categoryCards);
}
}
}
The State:
abstract class CategoryCardState
implements Built<CategoryCardState, CategoryCardStateBuilder> {
Future<BuiltList<CategoryCard>> get categoryCards;
//*Reached end indicator
bool get hasReachedEndOfDocuments;
//*Error state
String get exception;
//*Loading state
@nullable
bool get isLoading;
//*Success state
@nullable
bool get isSuccessful;
//*Loaded state
@nullable
bool get isLoaded;
CategoryCardState._();
factory CategoryCardState([updates(CategoryCardStateBuilder b)]) =
_$CategoryCardState;
factory CategoryCardState.intial() {
return CategoryCardState((b) => b
..exception = ''
..isSuccessful = false
..categoryCards =
Future<BuiltList<CategoryCard>>.value(BuiltList<CategoryCard>())
..hasReachedEndOfDocuments = false);
}
factory CategoryCardState.loading() {
return CategoryCardState((b) => b
..exception = ''
..categoryCards =
Future<BuiltList<CategoryCard>>.value(BuiltList<CategoryCard>())
..hasReachedEndOfDocuments = false
..isLoading = true);
}
factory CategoryCardState.loaded(Future<BuiltList<CategoryCard>> cards) {
return CategoryCardState((b) => b
..exception = ''
..categoryCards = cards
..hasReachedEndOfDocuments = false
..isLoading = false
..isLoaded = true);
}
factory CategoryCardState.success(Future<BuiltList<CategoryCard>> cards) {
return CategoryCardState((b) => b
..exception = ''
..categoryCards =
Future<BuiltList<CategoryCard>>.value(BuiltList<CategoryCard>())
..hasReachedEndOfDocuments = false
..isSuccessful = true);
}
factory CategoryCardState.failed(
String exception, Future<BuiltList<CategoryCard>> cards) {
return CategoryCardState((b) => b
..exception = exception
..categoryCards = cards
..hasReachedEndOfDocuments = false);
}
}
The event:
abstract class CategoryCardEvents extends Equatable {}
class LoadCategoryCardEvent extends CategoryCardEvents {
final DocumentSnapshot amountDocumentSnapshot;
LoadCategoryCardEvent({@required this.amountDocumentSnapshot});
@override
List<Object> get props => [amountDocumentSnapshot];
}
The pagination screen(Contained inside a stateful widget):
//Notification Handler
bool _scrollNotificationHandler(
ScrollNotification notification,
DocumentSnapshot amountDocumentSnapshot,
bool hasReachedEndOfDocuments,
Future<BuiltList<CategoryCard>> cards) {
if (notification is ScrollEndNotification &&
_scollControllerHomeScreen.position.extentAfter == 0 &&
!hasReachedEndOfDocuments) {
setState(() {
_hasReachedEnd = true;
});
_categoryCardBloc.add(LoadCategoryCardEvent(
amountDocumentSnapshot: amountDocumentSnapshot));
}
return false;
}
BlocListener<CategoryCardBloc, CategoryCardState>(
bloc: _categoryCardBloc,
listener: (context, state) {
if (state.exception != null &&
state.exception.isNotEmpty) {
if (state.exception == NullException.exceptionMessage) {
print('Null Exception');
} else {
ErrorDialogs.customAlertDialog(
context,
'Failed to load',
'Please restart app or contact support');
print(state.exception);
}
}
},
child: BlocBuilder<CategoryCardBloc, CategoryCardState>(
bloc: _categoryCardBloc,
builder: (context, state) {
if (state.isLoading != null && state.isLoading) {
return Center(
child: CustomLoader(),
);
}
if (state.isLoaded != null && state.isLoaded) {
return StreamBuilder<BuiltList<CategoryCard>>(
stream: state.categoryCards.asStream(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CustomLoader(),
);
} else {
BuiltList<CategoryCard> categoryCards =
snapshot.data;
_hasReachedEnd = false;
print(state.hasReachedEndOfDocuments &&
state.hasReachedEndOfDocuments != null);
return Container(
height: Mquery.screenHeight(context),
width: Mquery.screenWidth(context),
child: NotificationListener<
ScrollNotification>(
onNotification: (notification) =>
_scrollNotificationHandler(
notification,
categoryCards.last.document,
state.hasReachedEndOfDocuments,
state.categoryCards),
child: SingleChildScrollView(
controller: _scollControllerHomeScreen,
child: Column(
children: [
CustomAppBar(),
Padding(
padding: EdgeInsets.all(
Mquery.padding(context, 2.0)),
child: Row(
children: [
Expanded(
flex: 5,
child: Padding(
padding: EdgeInsets.all(
Mquery.padding(
context, 1.0)),
child:Container(
width: Mquery.width(
context, 50.0),
height: Mquery.width(
context, 12.5),
decoration:
BoxDecoration(
color: middle_black,
borderRadius: BorderRadius
.circular(Constants
.CARD_BORDER_RADIUS),
boxShadow: [
BoxShadow(
color: Colors
.black54,
blurRadius:
4.0,
spreadRadius:
0.5)
],
),
child: Padding(
padding: EdgeInsets.fromLTRB(
Mquery.padding(
context,
4.0),
Mquery.padding(
context,
4.0),
Mquery.padding(
context,
2.0),
Mquery.padding(
context,
1.0)),
child: TextField(
textInputAction:
TextInputAction
.done,
style: TextStyle(
color: white,
fontSize: Mquery
.fontSize(
context,
4.25)),
controller:
searchController,
decoration:
InputDecoration(
border:
InputBorder
.none,
hintText: Constants
.SEARCH_MESSAGE,
hintStyle: TextStyle(
fontSize: Mquery
.fontSize(
context,
4.25),
color:
white),
),
),
),
),
),
Expanded(
flex: 1,
child: Padding(
padding: EdgeInsets.all(
Mquery.padding(
context, 1.0)),
child: Container(
decoration:
BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors
.black54,
blurRadius:
4.0,
spreadRadius:
0.5)
],
color: middle_black,
borderRadius: BorderRadius
.circular(Constants
.CARD_BORDER_RADIUS),
),
width: Mquery.width(
context, 12.5),
height: Mquery.width(
context, 12.5),
child: IconButton(
splashColor: Colors
.transparent,
highlightColor:
Colors
.transparent,
icon: Icon(
Icons.search,
color: white,
),
onPressed: () {
_onSearchButtonPressed();
},
),
),
))
],
),
),
ListView.builder(
shrinkWrap: true,
itemCount: categoryCards.length,
physics:
NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
//Navigate
},
child:
CategoryCardWidget(
categoryCount:
categoryCards
.length,
categoryCard:
categoryCards[
index]));
},
),
_hasReachedEnd
? Padding(
padding: EdgeInsets.all(
Mquery.padding(
context, 4.0)),
child: CustomLoader(),
)
: Container()
],
),
),
),
);
}
},
);
}
return Container();
}))
Thank you for you time and sorry for being so verbose.
I managed to solve this problem,
Below is the Github link: https://github.com/felangel/bloc/issues/1707
Hope that helps someone! -Matt