Skip to content

Commit d8d8bf5

Browse files
feat(ui_firestore): implemented separators and fetching indicator (#99)
* Added the following features to FirestoreListView: - show separators between list items - show loading indicator when the list reaches the end - show both loading indicator and separators * Separate constructors withLoadingIndicator and separatedWithLoadingIndicator have been removed. This functionality is now achieved via constructor properties. * Added firebase_ui_shared * 1) pageSize default is set to 10 and it is not dependent on showLoadingIndicator anymore. 2) showLoadingIndicator is renamed to showFetchingIndicator. FirestoreLoadingIndicatorBuilder is also renamed to FirestoreFetchingIndicatorBuilder accordingly. 3) LoadingIndicator has been used from firebase_ui_shared package 4) Unnecessary comments deleted. 5) BuildAwareWidget is now called OnMountListener and its onBuild method is called onMount. 6) onMount can be called with a delay, that can be configured as per requirement. A default delay of 500ms has been used here to make the fetching indicator show for a bit of a time. * Deleted the conflicting lines * Updated firebase_ui_localizations to resolve conflict * fix ui_shared dependency version * fix: analyzer --------- Co-authored-by: Andrei Lesnitsky <[email protected]>
1 parent c0612cf commit d8d8bf5

File tree

2 files changed

+193
-4
lines changed

2 files changed

+193
-4
lines changed

packages/firebase_ui_firestore/lib/src/query_builder.dart

Lines changed: 192 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66

7+
import 'package:firebase_ui_shared/firebase_ui_shared.dart';
78
import 'package:flutter/gestures.dart';
89
import 'package:flutter/material.dart';
910
import 'package:cloud_firestore/cloud_firestore.dart';
@@ -358,6 +359,11 @@ typedef FirestoreItemBuilder<Document> = Widget Function(
358359
/// A type representing the function passed to [FirestoreListView] for its `loadingBuilder`.
359360
typedef FirestoreLoadingBuilder = Widget Function(BuildContext context);
360361

362+
/// A type representing the function passed to [FirestoreListView] for its `loadingIndicatorBuilder`.
363+
typedef FirestoreFetchingIndicatorBuilder = Widget Function(
364+
BuildContext context,
365+
);
366+
361367
/// A type representing the function passed to [FirestoreListView] for its `errorBuilder`.
362368
typedef FirestoreErrorBuilder = Widget Function(
363369
BuildContext context,
@@ -427,9 +433,11 @@ class FirestoreListView<Document> extends FirestoreQueryBuilder<Document> {
427433
required FirestoreItemBuilder<Document> itemBuilder,
428434
super.pageSize,
429435
FirestoreLoadingBuilder? loadingBuilder,
436+
FirestoreFetchingIndicatorBuilder? fetchingIndicatorBuilder,
430437
FirestoreErrorBuilder? errorBuilder,
431438
FirestoreEmptyBuilder? emptyBuilder,
432439
Axis scrollDirection = Axis.vertical,
440+
bool showFetchingIndicator = false,
433441
bool reverse = false,
434442
ScrollController? controller,
435443
bool? primary,
@@ -467,14 +475,45 @@ class FirestoreListView<Document> extends FirestoreQueryBuilder<Document> {
467475
return emptyBuilder(context);
468476
}
469477

478+
final itemCount = snapshot.docs.length;
479+
470480
return ListView.builder(
471-
itemCount: snapshot.docs.length,
481+
itemCount: itemCount,
472482
itemBuilder: (context, index) {
473-
final isLastItem = index + 1 == snapshot.docs.length;
474-
if (isLastItem && snapshot.hasMore) snapshot.fetchMore();
483+
final isLastItem = index + 1 == itemCount;
484+
if (!showFetchingIndicator && isLastItem && snapshot.hasMore) {
485+
snapshot.fetchMore();
486+
}
475487

476488
final doc = snapshot.docs[index];
477-
return itemBuilder(context, doc);
489+
return showFetchingIndicator
490+
? OnMountListener(
491+
onMount: () {
492+
if (isLastItem && snapshot.hasMore) {
493+
snapshot.fetchMore();
494+
}
495+
},
496+
child: Column(
497+
crossAxisAlignment: CrossAxisAlignment.start,
498+
children: [
499+
itemBuilder(context, doc),
500+
if (isLastItem && snapshot.hasMore)
501+
fetchingIndicatorBuilder?.call(context) ??
502+
const Padding(
503+
padding: EdgeInsets.symmetric(
504+
vertical: 16.0,
505+
),
506+
child: Center(
507+
child: LoadingIndicator(
508+
size: 30.0,
509+
borderWidth: 2.0,
510+
),
511+
),
512+
),
513+
],
514+
),
515+
)
516+
: itemBuilder(context, doc);
478517
},
479518
scrollDirection: scrollDirection,
480519
reverse: reverse,
@@ -497,6 +536,115 @@ class FirestoreListView<Document> extends FirestoreQueryBuilder<Document> {
497536
);
498537
},
499538
);
539+
540+
/// Shows a separator between list items just as in [ListView.separated]
541+
FirestoreListView.separated({
542+
super.key,
543+
required super.query,
544+
required FirestoreItemBuilder<Document> itemBuilder,
545+
super.pageSize,
546+
FirestoreLoadingBuilder? loadingBuilder,
547+
FirestoreFetchingIndicatorBuilder? fetchingIndicatorBuilder,
548+
FirestoreErrorBuilder? errorBuilder,
549+
FirestoreEmptyBuilder? emptyBuilder,
550+
required IndexedWidgetBuilder separatorBuilder,
551+
Axis scrollDirection = Axis.vertical,
552+
bool showFetchingIndicator = false,
553+
bool reverse = false,
554+
ScrollController? controller,
555+
bool? primary,
556+
ScrollPhysics? physics,
557+
bool shrinkWrap = false,
558+
EdgeInsetsGeometry? padding,
559+
ChildIndexGetter? findChildIndexCallback,
560+
bool addAutomaticKeepAlives = true,
561+
bool addRepaintBoundaries = true,
562+
bool addSemanticIndexes = true,
563+
double? cacheExtent,
564+
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
565+
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior =
566+
ScrollViewKeyboardDismissBehavior.manual,
567+
String? restorationId,
568+
Clip clipBehavior = Clip.hardEdge,
569+
}) : super(
570+
builder: (context, snapshot, _) {
571+
if (snapshot.isFetching) {
572+
return loadingBuilder?.call(context) ??
573+
const Center(child: CircularProgressIndicator());
574+
}
575+
576+
if (snapshot.hasError && errorBuilder != null) {
577+
return errorBuilder(
578+
context,
579+
snapshot.error!,
580+
snapshot.stackTrace!,
581+
);
582+
}
583+
584+
if (snapshot.docs.isEmpty && emptyBuilder != null) {
585+
return emptyBuilder(context);
586+
}
587+
588+
final itemCount = snapshot.docs.length;
589+
590+
return ListView.separated(
591+
itemCount: itemCount,
592+
itemBuilder: (context, index) {
593+
final isLastItem = index + 1 == itemCount;
594+
if (!showFetchingIndicator && isLastItem && snapshot.hasMore) {
595+
snapshot.fetchMore();
596+
}
597+
598+
final doc = snapshot.docs[index];
599+
return showFetchingIndicator
600+
? OnMountListener(
601+
onMount: () {
602+
if (isLastItem && snapshot.hasMore) {
603+
snapshot.fetchMore();
604+
}
605+
},
606+
child: Column(
607+
crossAxisAlignment: CrossAxisAlignment.start,
608+
children: [
609+
itemBuilder(context, doc),
610+
if (isLastItem && snapshot.hasMore)
611+
fetchingIndicatorBuilder?.call(context) ??
612+
const Padding(
613+
padding: EdgeInsets.symmetric(
614+
vertical: 16.0,
615+
),
616+
child: Center(
617+
child: LoadingIndicator(
618+
size: 30.0,
619+
borderWidth: 2.0,
620+
),
621+
),
622+
),
623+
],
624+
),
625+
)
626+
: itemBuilder(context, doc);
627+
},
628+
separatorBuilder: separatorBuilder,
629+
scrollDirection: scrollDirection,
630+
reverse: reverse,
631+
controller: controller,
632+
primary: primary,
633+
physics: physics,
634+
shrinkWrap: shrinkWrap,
635+
padding: padding,
636+
findChildIndexCallback: findChildIndexCallback,
637+
addAutomaticKeepAlives: addAutomaticKeepAlives,
638+
addRepaintBoundaries: addRepaintBoundaries,
639+
addSemanticIndexes: addSemanticIndexes,
640+
cacheExtent: cacheExtent,
641+
dragStartBehavior: dragStartBehavior,
642+
keyboardDismissBehavior: keyboardDismissBehavior,
643+
restorationId: restorationId,
644+
clipBehavior: clipBehavior,
645+
);
646+
},
647+
);
500648
}
501649

502650
/// Listens to an aggregate query and passes the [AsyncSnapshot] to the builder.
@@ -541,3 +689,43 @@ class _AggregateQueryBuilderState extends State<AggregateQueryBuilder> {
541689
}
542690
}
543691
}
692+
693+
/// This widget calls back, via the supplied onMount method, when it gets
694+
/// mounted.
695+
/// It also offers the functionality to safely delay the onMount callback by
696+
/// onMountDelay.
697+
///
698+
/// Borrowed the idea from the link below and built on it further:
699+
/// https://www.filledstacks.com/post/how-to-perform-real-time-pagination-with-firestore/?utm_source=pocket_reader
700+
class OnMountListener extends StatefulWidget {
701+
final Function onMount;
702+
final int onMountDelay; // in milliseconds
703+
final Widget child;
704+
705+
const OnMountListener({
706+
super.key,
707+
required this.onMount,
708+
this.onMountDelay = 500,
709+
required this.child,
710+
});
711+
712+
@override
713+
State<OnMountListener> createState() => _OnMountListenerState();
714+
}
715+
716+
class _OnMountListenerState extends State<OnMountListener> {
717+
@override
718+
void initState() {
719+
super.initState();
720+
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
721+
Future.delayed(Duration(milliseconds: widget.onMountDelay), () {
722+
if (mounted) widget.onMount();
723+
});
724+
});
725+
}
726+
727+
@override
728+
Widget build(BuildContext context) {
729+
return widget.child;
730+
}
731+
}

packages/firebase_ui_firestore/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ environment:
1010
dependencies:
1111
cloud_firestore: ^4.13.3
1212
firebase_ui_localizations: ^1.9.0
13+
firebase_ui_shared: ^1.4.1
1314
flutter:
1415
sdk: flutter
1516

0 commit comments

Comments
 (0)