4
4
5
5
import 'dart:async' ;
6
6
7
+ import 'package:firebase_ui_shared/firebase_ui_shared.dart' ;
7
8
import 'package:flutter/gestures.dart' ;
8
9
import 'package:flutter/material.dart' ;
9
10
import 'package:cloud_firestore/cloud_firestore.dart' ;
@@ -358,6 +359,11 @@ typedef FirestoreItemBuilder<Document> = Widget Function(
358
359
/// A type representing the function passed to [FirestoreListView] for its `loadingBuilder` .
359
360
typedef FirestoreLoadingBuilder = Widget Function (BuildContext context);
360
361
362
+ /// A type representing the function passed to [FirestoreListView] for its `loadingIndicatorBuilder` .
363
+ typedef FirestoreFetchingIndicatorBuilder = Widget Function (
364
+ BuildContext context,
365
+ );
366
+
361
367
/// A type representing the function passed to [FirestoreListView] for its `errorBuilder` .
362
368
typedef FirestoreErrorBuilder = Widget Function (
363
369
BuildContext context,
@@ -427,9 +433,11 @@ class FirestoreListView<Document> extends FirestoreQueryBuilder<Document> {
427
433
required FirestoreItemBuilder <Document > itemBuilder,
428
434
super .pageSize,
429
435
FirestoreLoadingBuilder ? loadingBuilder,
436
+ FirestoreFetchingIndicatorBuilder ? fetchingIndicatorBuilder,
430
437
FirestoreErrorBuilder ? errorBuilder,
431
438
FirestoreEmptyBuilder ? emptyBuilder,
432
439
Axis scrollDirection = Axis .vertical,
440
+ bool showFetchingIndicator = false ,
433
441
bool reverse = false ,
434
442
ScrollController ? controller,
435
443
bool ? primary,
@@ -467,14 +475,45 @@ class FirestoreListView<Document> extends FirestoreQueryBuilder<Document> {
467
475
return emptyBuilder (context);
468
476
}
469
477
478
+ final itemCount = snapshot.docs.length;
479
+
470
480
return ListView .builder (
471
- itemCount: snapshot.docs.length ,
481
+ itemCount: itemCount ,
472
482
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
+ }
475
487
476
488
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);
478
517
},
479
518
scrollDirection: scrollDirection,
480
519
reverse: reverse,
@@ -497,6 +536,115 @@ class FirestoreListView<Document> extends FirestoreQueryBuilder<Document> {
497
536
);
498
537
},
499
538
);
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
+ );
500
648
}
501
649
502
650
/// Listens to an aggregate query and passes the [AsyncSnapshot] to the builder.
@@ -541,3 +689,43 @@ class _AggregateQueryBuilderState extends State<AggregateQueryBuilder> {
541
689
}
542
690
}
543
691
}
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
+ }
0 commit comments