INFRA: Set Up Project.

This commit is contained in:
2025-11-28 11:10:49 +05:00
commit c798279f7d
609 changed files with 77436 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
/// A package to paginate your firebase related data with realtime updates.
///
/// It can be used for `Firestore` and `Firebase Realtime Database`.
///
/// Data can be shown in `list`, `grid` and `scrollable wrap` view.
library;
export 'src/firestore_pagination.dart';
// Data Models
export 'src/models/view_type.dart';
export 'src/models/wrap_options.dart';
// Widgets
export 'src/realtime_db_pagination.dart';

View File

@@ -0,0 +1,335 @@
// Dart Packages
import 'dart:async';
// Firebase Packages
import 'package:cloud_firestore/cloud_firestore.dart';
// Flutter Packages
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
// Functions
import 'functions/separator_builder.dart';
// Data Models
import 'models/page_options.dart';
import 'models/view_type.dart';
import 'models/wrap_options.dart';
// Widgets
import 'widgets/defaults/bottom_loader.dart';
import 'widgets/defaults/empty_screen.dart';
import 'widgets/defaults/initial_loader.dart';
import 'widgets/views/build_pagination.dart';
/// A [StreamBuilder] that automatically loads more data when the user scrolls
/// to the bottom.
///
/// Optimized for [FirebaseFirestore] with fields like `createdAt` and
/// `timestamp` to sort the data.
///
/// Supports live updates and realtime updates to loaded data.
///
/// Data can be represented in a [ListView], [GridView] or scollable [Wrap].
class FirestorePagination extends StatefulWidget {
/// Creates a [StreamBuilder] widget that automatically loads more data when
/// the user scrolls to the bottom.
///
/// Optimized for [FirebaseFirestore] with fields like `createdAt` and
/// `timestamp` to sort the data.
///
/// Supports live updates and realtime updates to loaded data.
///
/// Data can be represented in a [ListView], [GridView] or scollable [Wrap].
const FirestorePagination({
required this.query,
required this.itemBuilder,
super.key,
this.separatorBuilder,
this.limit = 10,
this.viewType = ViewType.list,
this.isLive = false,
this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
this.wrapOptions = const WrapOptions(),
this.pageOptions = const PageOptions(),
this.onEmpty = const EmptyScreen(),
this.bottomLoader = const BottomLoader(),
this.initialLoader = const InitialLoader(),
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.shrinkWrap = false,
this.physics,
this.padding,
this.controller,
this.pageController,
});
/// The query to use to fetch data from Firestore.
///
/// ### Note:
/// - The query must **NOT** contain a `limit` itself.
/// - The `limit` must be set using the [limit] property of this widget.
final Query query;
/// The builder to use to build the items in the list.
///
/// The builder is passed the build context, snapshot of the document and
/// index of the item in the list.
final Widget Function(BuildContext, List<DocumentSnapshot>, int) itemBuilder;
/// The builder to use to render the separator.
///
/// Only used if [viewType] is [ViewType.list].
///
/// Default [Widget] is [SizedBox.shrink].
final Widget Function(BuildContext, int)? separatorBuilder;
/// The number of items to fetch from Firestore at once.
///
/// Defaults to `10`.
final int limit;
/// The type of view to use for the list.
///
/// Defaults to [ViewType.list].
final ViewType viewType;
/// Whether to fetch newly added items as they are added to Firestore.
///
/// Defaults to `false`.
final bool isLive;
/// The delegate to use for the [GridView].
///
/// Defaults to [SliverGridDelegateWithFixedCrossAxisCount].
final SliverGridDelegate gridDelegate;
/// The [Wrap] widget properties to use.
///
/// Defaults to [WrapOptions].
final WrapOptions wrapOptions;
/// The [PageView] properties to use.
///
/// Defaults to [PageOptions].
final PageOptions pageOptions;
/// The widget to use when data is empty.
///
/// Defaults to [EmptyScreen].
final Widget onEmpty;
/// The widget to use when more data is loading.
///
/// Defaults to [BottomLoader].
final Widget bottomLoader;
/// The widget to use when data is loading initially.
///
/// Defaults to [InitialLoader].
final Widget initialLoader;
/// The scrolling direction of the [ScrollView].
final Axis scrollDirection;
/// Whether the [ScrollView] scrolls in the reading direction.
final bool reverse;
/// Should the [ScrollView] be shrink-wrapped.
final bool shrinkWrap;
/// The scroll behavior to use for the [ScrollView].
final ScrollPhysics? physics;
/// The padding to use for the [ScrollView].
final EdgeInsetsGeometry? padding;
/// The scroll controller to use for the [ScrollView].
///
/// Defaults to [ScrollController].
final ScrollController? controller;
/// The page controller to use for the [PageView].
///
/// Defaults to [PageController].
final PageController? pageController;
@override
State<FirestorePagination> createState() => _FirestorePaginationState();
}
/// The state of the [FirestorePagination] widget.
class _FirestorePaginationState extends State<FirestorePagination> {
/// All the data that has been loaded from Firestore.
final List<DocumentSnapshot> _docs = [];
/// Snapshot subscription for the query.
///
/// Also handles updates to loaded data.
StreamSubscription<QuerySnapshot>? _streamSub;
/// Snapshot subscription for the query to handle newly added data.
StreamSubscription<QuerySnapshot>? _liveStreamSub;
/// [ScrollController] to listen to scroll end and load more data.
late final ScrollController _controller = widget.controller ?? ScrollController();
/// [PageController] to listen to page changes and load more data.
late final PageController _pageController = widget.pageController ?? PageController();
/// Whether initial data is loading.
bool _isInitialLoading = true;
/// Whether more data is loading.
bool _isFetching = false;
/// Whether the end for given query has been reached.
///
/// This is used to determine if more data should be loaded when the user
/// scrolls to the bottom.
bool _isEnded = false;
/// Loads more data from Firestore and handles updates to loaded data.
///
/// Setting [getMore] to `false` will only set listener for the currently
/// loaded data.
Future<void> _loadDocuments({bool getMore = true}) async {
// To cancel previous updates listener when new one is set.
final tempSub = _streamSub;
if (getMore) setState(() => _isFetching = true);
final docsLimit = _docs.length + (getMore ? widget.limit : 0);
var docsQuery = widget.query.limit(docsLimit);
if (_docs.isNotEmpty) {
docsQuery = docsQuery.startAtDocument(_docs.first);
}
_streamSub = docsQuery.snapshots().listen((QuerySnapshot snapshot) async {
await tempSub?.cancel();
_docs
..clear()
..addAll(snapshot.docs);
// To set new updates listener for the existing data
// or to set new live listener if the first document is removed.
final isDocRemoved = snapshot.docChanges.any(
(DocumentChange change) => change.type == DocumentChangeType.removed,
);
_isFetching = false;
if (!isDocRemoved) {
_isEnded = snapshot.docs.length < docsLimit;
}
if (isDocRemoved || _isInitialLoading) {
_isInitialLoading = false;
if (snapshot.docs.isNotEmpty) {
// Set updates listener for the existing data starting from the first
// document only.
await _loadDocuments(getMore: false);
} else {
_streamSub?.cancel();
}
if (widget.isLive) _setLiveListener();
}
if (mounted) setState(() {});
// Add data till the view is scrollable. This ensures that the user can
// scroll to the bottom and load more data.
if (_isInitialLoading || _isFetching || _isEnded) return;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (_controller.hasClients && _controller.position.maxScrollExtent <= 0) {
_loadDocuments();
}
});
});
}
/// Sets the live listener for the query.
///
/// Fires when new data is added to the query.
Future<void> _setLiveListener() async {
// To cancel previous live listener when new one is set.
final tempSub = _liveStreamSub;
var latestDocQuery = widget.query.limit(1);
if (_docs.isNotEmpty) {
latestDocQuery = latestDocQuery.endBeforeDocument(_docs.first);
}
_liveStreamSub = latestDocQuery.snapshots(includeMetadataChanges: true).listen(
(QuerySnapshot snapshot) async {
await tempSub?.cancel();
if (snapshot.docs.isEmpty || snapshot.docs.first.metadata.hasPendingWrites) return;
_docs.insert(0, snapshot.docs.first);
// To handle newly added data after this curently loaded data.
await _setLiveListener();
// Set updates listener for the newly added data.
_loadDocuments(getMore: false);
},
);
}
/// To handle scroll end event and load more data.
void _scrollListener() {
if (_isInitialLoading || _isFetching || _isEnded) return;
if (!_controller.hasClients) return;
final position = _controller.position;
if (position.pixels >= (position.maxScrollExtent - 50)) {
_loadDocuments();
}
}
@override
void initState() {
super.initState();
_loadDocuments();
_controller.addListener(_scrollListener);
}
@override
void dispose() {
_streamSub?.cancel();
_liveStreamSub?.cancel();
_controller
..removeListener(_scrollListener)
..dispose();
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _isInitialLoading
? widget.initialLoader
: _docs.isEmpty
? widget.onEmpty
: BuildPagination(
items: _docs,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder ?? separatorBuilder,
isLoading: _isFetching,
viewType: widget.viewType,
bottomLoader: widget.bottomLoader,
gridDelegate: widget.gridDelegate,
wrapOptions: widget.wrapOptions,
pageOptions: widget.pageOptions,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: _controller,
pageController: _pageController,
shrinkWrap: widget.shrinkWrap,
physics: widget.physics,
padding: widget.padding,
onPageChanged: (index) {
if (index >= _docs.length - 1) _loadDocuments();
},
);
}
}

View File

@@ -0,0 +1,7 @@
// Flutter Packages
import 'package:flutter/widgets.dart';
/// Returns a [Widget] to be render as separator in a [ListView].
Widget separatorBuilder(BuildContext context, int index) {
return const SizedBox.shrink();
}

View File

@@ -0,0 +1,69 @@
// Flutter Packages
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
// Data Models
import 'view_type.dart';
/// The properties of the [PageView] widget in the [ViewType.page] view.
class PageOptions {
/// Creates a object that contains the properties of the [PageView] widget.
const PageOptions({
this.clipBehavior = Clip.hardEdge,
this.pageSnapping = true,
this.padEnds = true,
this.scrollBehavior,
this.allowImplicitScrolling = false,
this.dragStartBehavior = DragStartBehavior.start,
});
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// Set to false to disable page snapping, useful for custom scroll behavior.
///
/// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
/// the page will snap to the beginning of the viewport; otherwise, the page
/// will snap to the center of the viewport.
final bool pageSnapping;
/// Whether to add padding to both ends of the list.
///
/// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
/// such that the first and last child slivers will be in the center of
/// the viewport when scrolled all the way to the start or end, respectively.
///
/// If [PageController.viewportFraction] >= 1.0, this property has no effect.
///
/// This property defaults to true.
final bool padEnds;
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [physics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to not apply a [Scrollbar].
final ScrollBehavior? scrollBehavior;
/// Controls whether the widget's pages will respond to
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
/// scrolling.
///
/// With this flag set to false, when accessibility focus reaches the end of
/// the current page and the user attempts to move it to the next element, the
/// focus will traverse to the next widget outside of the page view.
///
/// With this flag set to true, when accessibility focus reaches the end of
/// the current page and user attempts to move it to the next element, focus
/// will traverse to the next page in the page view.
final bool allowImplicitScrolling;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
}

View File

@@ -0,0 +1,19 @@
// Flutter Packages
import 'package:flutter/widgets.dart';
/// The [ScrollView] to use for the loaded data.
///
/// Supports [list], [grid], and [wrap].
enum ViewType {
/// Loads the data as a [ListView].
list,
/// Loads the data as a [GridView].
grid,
/// Loads the data as a scrollable [Wrap].
wrap,
/// Loads the data as a [PageView].
page,
}

View File

@@ -0,0 +1,67 @@
// Flutter Packages
import 'package:flutter/widgets.dart';
// Data Models
import 'view_type.dart';
/// The properties of the [Wrap] widget in the [ViewType.wrap] view.
class WrapOptions {
/// Creates a object that contains the properties of the [Wrap] widget.
const WrapOptions({
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.center,
this.spacing = 5.0,
this.runAlignment = WrapAlignment.start,
this.runSpacing = 5.0,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
this.clipBehavior = Clip.none,
});
/// The direction to use as the main axis.
///
/// Defaults to [Axis.horizontal].
final Axis direction;
/// How the children within a run should be placed in the main axis.
///
/// Defaults to [WrapAlignment.center].
final WrapAlignment alignment;
/// How much space to place between children in a run in the main axis.
///
/// Defaults to 5.0.
final double spacing;
/// How the runs themselves should be placed in the cross axis.
///
/// Defaults to [WrapAlignment.start].
final WrapAlignment runAlignment;
/// How much space to place between the runs themselves in the cross axis.
///
/// Defaults to 5.0.
final double runSpacing;
/// How the children within a run should be aligned relative to each other in
/// the cross axis.
///
/// Defaults to [WrapCrossAlignment.start].
final WrapCrossAlignment crossAxisAlignment;
/// Determines the order to lay children out horizontally and how to interpret
/// `start` and `end` in the horizontal direction.
final TextDirection? textDirection;
/// Determines the order to lay children out vertically and how to interpret
/// `start` and `end` in the vertical direction.
///
/// Defaults to [VerticalDirection.down].
final VerticalDirection verticalDirection;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none].
final Clip clipBehavior;
}

View File

@@ -0,0 +1,400 @@
// Dart Packages
import 'dart:async';
// Firebase Packages
import 'package:firebase_database/firebase_database.dart';
// Flutter Packages
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
// Functions
import 'functions/separator_builder.dart';
// Data Models
import 'models/page_options.dart';
import 'models/view_type.dart';
import 'models/wrap_options.dart';
// Widgets
import 'widgets/defaults/bottom_loader.dart';
import 'widgets/defaults/empty_screen.dart';
import 'widgets/defaults/initial_loader.dart';
import 'widgets/views/build_pagination.dart';
/// A [StreamBuilder] that automatically loads more data when the user scrolls
/// to the bottom.
///
/// Optimized for [FirebaseDatabase] with fields like `createdAt` and
/// `timestamp` to sort the data.
///
/// Supports live updates and realtime updates to loaded data.
///
/// Data can be represented in a [ListView], [GridView] or scollable [Wrap].
class RealtimeDBPagination extends StatefulWidget {
/// Creates a [StreamBuilder] widget that automatically loads more data when
/// the user scrolls to the bottom.
///
/// Optimized for [FirebaseDatabase] with fields like `createdAt` and
/// `timestamp` to sort the data.
///
/// Supports live updates and realtime updates to loaded data.
///
/// Data can be represented in a [ListView], [GridView] or scollable [Wrap].
const RealtimeDBPagination({
required this.query,
required this.itemBuilder,
required this.orderBy,
super.key,
this.descending = false,
this.separatorBuilder,
this.limit = 10,
this.viewType = ViewType.list,
this.isLive = false,
this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
this.wrapOptions = const WrapOptions(),
this.pageOptions = const PageOptions(),
this.onEmpty = const EmptyScreen(),
this.bottomLoader = const BottomLoader(),
this.initialLoader = const InitialLoader(),
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.shrinkWrap = false,
this.physics,
this.padding,
this.controller,
this.pageController,
});
/// The query to use to fetch data from Firebase Realtime Database.
///
/// ### Note:
/// - The query must **NOT** contain a `limitToFirst` or `limitToLast` itself.
/// - The `limit` must be set using the [limit] property of this widget.
final Query query;
/// The builder to use to build the items in the list.
///
/// The builder is passed the build context, snapshot of data and index of
/// the item in the list.
final Widget Function(BuildContext, List<DataSnapshot>, int) itemBuilder;
/// The field to use to sort the data. Give the same value as the field
/// used to order the data in the query.
///
/// ## Example
/// If the query is:
/// ```dart
/// FirebaseDatabase.instance.ref('messages').orderByChild('createdAt')
/// ```
/// Then the value of [orderBy] should be `createdAt`.
///
/// If null, the data will be sorted by the key.
final String? orderBy;
/// Fetches data is decending order for the given [orderBy] field.
///
/// Default value is `false`.
final bool descending;
/// The builder to use to render the separator.
///
/// Only used if [viewType] is [ViewType.list].
///
/// Default [Widget] is [SizedBox.shrink].
final Widget Function(BuildContext, int)? separatorBuilder;
/// The number of items to fetch from Firebase Realtime Database at once.
///
/// Defaults to `10`.
final int limit;
/// The type of view to use for the list.
///
/// Defaults to [ViewType.list].
final ViewType viewType;
/// Whether to fetch newly added items as they are added to
/// Firebase Realtime Database.
///
/// Defaults to `false`.
final bool isLive;
/// The delegate to use for the [GridView].
///
/// Defaults to [SliverGridDelegateWithFixedCrossAxisCount].
final SliverGridDelegate gridDelegate;
/// The [Wrap] widget properties to use.
///
/// Defaults to [WrapOptions].
final WrapOptions wrapOptions;
/// The [PageView] properties to use.
///
/// Defaults to [PageOptions].
final PageOptions pageOptions;
/// The widget to use when data is empty.
///
/// Defaults to [EmptyScreen].
final Widget onEmpty;
/// The widget to use when more data is loading.
///
/// Defaults to [BottomLoader].
final Widget bottomLoader;
/// The widget to use when data is loading initially.
///
/// Defaults to [InitialLoader].
final Widget initialLoader;
/// The scrolling direction of the [ScrollView].
final Axis scrollDirection;
/// Whether the [ScrollView] scrolls in the reading direction.
final bool reverse;
/// Should the [ScrollView] be shrink-wrapped.
final bool shrinkWrap;
/// The scroll behavior to use for the [ScrollView].
final ScrollPhysics? physics;
/// The padding to use for the [ScrollView].
final EdgeInsetsGeometry? padding;
/// The scroll controller to use for the [ScrollView].
///
/// Defaults to [ScrollController].
final ScrollController? controller;
/// The page controller to use for the [PageView].
///
/// Defaults to [PageController].
final PageController? pageController;
@override
State<RealtimeDBPagination> createState() => _RealtimeDBPaginationState();
}
/// The state of the [RealtimeDBPagination] widget.
class _RealtimeDBPaginationState extends State<RealtimeDBPagination> {
/// All the data that has been loaded from Firebase Realtime Database.
final List<DataSnapshot> _data = [];
/// Snapshot subscription for the query.
///
/// Also handles updates to loaded data.
StreamSubscription<DatabaseEvent>? _streamSub;
/// Snapshot subscription for the query to handle newly added data.
StreamSubscription<DatabaseEvent>? _liveStreamSub;
/// [ScrollController] to listen to scroll end and load more data.
late final ScrollController _controller = widget.controller ?? ScrollController();
/// [PageController] to listen to page changes and load more data.
late final PageController _pageController = widget.pageController ?? PageController();
/// Whether initial data is loading.
bool _isInitialLoading = true;
/// Whether more data is loading.
bool _isFetching = false;
/// Whether the end for given query has been reached.
///
/// This is used to determine if more data should be loaded when the user
/// scrolls to the bottom.
bool _isEnded = false;
/// Loads more data from Firebase Realtime Database and handles
/// updates to loaded data.
///
/// Setting [getMore] to `false` will only set listener for the
/// currently loaded data.
Future<void> _loadData({bool getMore = true}) async {
// To cancel previous updates listener when new one is set.
final tempSub = _streamSub;
if (getMore) setState(() => _isFetching = true);
// Sets limit of nodes to fetch.
// If currently 15 items are loaded, and limit is 5 then total 20 items
// will be fetched including the ones already present.
final docsLimit = _data.length + (getMore ? widget.limit : 0);
var docsQuery = widget.descending ? widget.query.limitToLast(docsLimit) : widget.query.limitToFirst(docsLimit);
if (_data.isNotEmpty) {
if (widget.descending) {
// Sets ending point from where before data should be fetched.
// If currently 15 items are loaded, and limit is 5 then total 20 items
// will be fetched where below mentioned value will be the largest and
// last in the fetched array (But first in callback array as using
// reversed in build method)
docsQuery = docsQuery.endAt(
Map<String, dynamic>.from(
_data.last.value! as Map<Object?, Object?>,
)[widget.orderBy],
);
} else {
// Sets starting point from where after data should be fetched.
// If currently 15 items are loaded, and limit is 5 then total 20 items
// will be fetched where below mentioned value will be the smallest and
// first in array
docsQuery = docsQuery.startAt(
Map<String, dynamic>.from(
_data.first.value! as Map<Object?, Object?>,
)[widget.orderBy],
);
}
}
_streamSub = docsQuery.onValue.listen((DatabaseEvent snapshot) async {
await tempSub?.cancel();
_data
..clear()
..addAll(snapshot.snapshot.children);
// To set new updates listener for the existing data
// or to set new live listener if the first data node is removed.
final isDataRemoved = snapshot.type == DatabaseEventType.childRemoved;
_isFetching = false;
if (!isDataRemoved) {
_isEnded = snapshot.snapshot.children.length < docsLimit;
}
if (isDataRemoved || _isInitialLoading) {
_isInitialLoading = false;
if (snapshot.snapshot.children.isNotEmpty) {
// Set updates listener for the existing data starting from the
// first data node only.
await _loadData(getMore: false);
} else {
_streamSub?.cancel();
}
if (widget.isLive) _setLiveListener();
}
if (mounted) setState(() {});
// Add data till the view is scrollable. This ensures that the user can
// scroll to the bottom and load more data.
if (_isInitialLoading || _isFetching || _isEnded) return;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (_controller.hasClients && _controller.position.maxScrollExtent <= 0) {
_loadData();
}
});
});
}
/// Sets the live listener for the query.
///
/// Fires when new data is added to the query.
Future<void> _setLiveListener() async {
// To cancel previous live listener when new one is set.
final tempSub = _liveStreamSub;
var latestDocQuery = widget.descending ? widget.query.limitToLast(1) : widget.query.limitToFirst(1);
if (_data.isNotEmpty) {
if (widget.descending) {
// Sets query to fetch data after the last element in the array,
// which is the largest value.
latestDocQuery = latestDocQuery.startAfter(
Map<String, dynamic>.from(
_data.last.value! as Map<Object?, Object?>,
)[widget.orderBy],
);
} else {
// Sets query to fetch data before the first element in the array,
// whch is the smallest value
latestDocQuery = latestDocQuery.endBefore(
Map<String, dynamic>.from(
_data.first.value! as Map<Object?, Object?>,
)[widget.orderBy],
);
}
}
_liveStreamSub = latestDocQuery.onValue.listen(
(DatabaseEvent snapshot) async {
await tempSub?.cancel();
if (snapshot.snapshot.children.isEmpty) return;
_data.insert(
widget.descending ? _data.length : 0,
snapshot.snapshot.children.first,
);
// To handle newly added data after this curently loaded data.
await _setLiveListener();
// Set updates listener for the newly added data.
_loadData(getMore: false);
},
);
}
/// To handle scroll end event and load more data.
void _scrollListener() {
if (_isInitialLoading || _isFetching || _isEnded) return;
if (!_controller.hasClients) return;
final position = _controller.position;
if (position.pixels >= (position.maxScrollExtent - 50)) {
_loadData();
}
}
@override
void initState() {
super.initState();
_loadData();
_controller.addListener(_scrollListener);
}
@override
void dispose() {
_streamSub?.cancel();
_liveStreamSub?.cancel();
_controller
..removeListener(_scrollListener)
..dispose();
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _isInitialLoading
? widget.initialLoader
: _data.isEmpty
? widget.onEmpty
: BuildPagination(
items: widget.descending ? _data.reversed.toList() : _data,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder ?? separatorBuilder,
isLoading: _isFetching,
viewType: widget.viewType,
bottomLoader: widget.bottomLoader,
gridDelegate: widget.gridDelegate,
wrapOptions: widget.wrapOptions,
pageOptions: widget.pageOptions,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
controller: _controller,
pageController: _pageController,
shrinkWrap: widget.shrinkWrap,
physics: widget.physics,
padding: widget.padding,
onPageChanged: (index) {
if (index >= _data.length - 1) _loadData();
},
);
}
}

View File

@@ -0,0 +1,28 @@
// Flutter Packages
import 'package:flutter/material.dart';
/// A circular progress indicator that spins when the [Stream] is loading.
///
/// Used at the bottom of a [ScrollView] to indicate that more data is loading.
class BottomLoader extends StatelessWidget {
/// Creates a circular progress indicator that spins when the [Stream] is
/// loading.
///
/// Used at the bottom of a [ScrollView] to indicate that more data is
/// loading.
const BottomLoader({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 25,
height: 25,
margin: const EdgeInsets.all(10),
child: const CircularProgressIndicator.adaptive(
strokeWidth: 2.5,
),
),
);
}
}

View File

@@ -0,0 +1,16 @@
// Flutter Packages
import 'package:flutter/material.dart';
import 'package:get/get.dart';
/// A [Widget] to show when there is no data to display.
class EmptyScreen extends StatelessWidget {
/// Creates a [Widget] to show when there is no data to display.
const EmptyScreen({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text('Nothing found here...'.tr),
);
}
}

View File

@@ -0,0 +1,20 @@
// Flutter Packages
import 'package:flutter/material.dart';
/// A circular progress indicator that spins when the [Stream] is loading.
///
/// Used when the [Stream] is loading the first time.
class InitialLoader extends StatelessWidget {
/// Creates a circular progress indicator that spins when the [Stream] is
/// loading.
///
/// Used when the [Stream] is loading the first time.
const InitialLoader({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
}

View File

@@ -0,0 +1,177 @@
// Flutter Packages
import 'package:customer/widget/firebase_pagination/src/models/page_options.dart';
import 'package:customer/widget/firebase_pagination/src/models/view_type.dart';
import 'package:customer/widget/firebase_pagination/src/models/wrap_options.dart';
import 'package:flutter/material.dart';
/// A [ScrollView] to use for the provided [items].
///
/// The [items] are loaded into the [ScrollView] based on the [viewType].
class BuildPagination<T> extends StatelessWidget {
/// Creates a [ScrollView] to use for the provided [items].
///
/// The [items] are rendered in the [ScrollView] using the [itemBuilder].
///
/// The [viewType] determines the type of [ScrollView] to use.
const BuildPagination({
super.key,
required this.items,
required this.itemBuilder,
required this.separatorBuilder,
required this.isLoading,
required this.viewType,
required this.bottomLoader,
required this.gridDelegate,
required this.wrapOptions,
required this.pageOptions,
required this.scrollDirection,
required this.reverse,
required this.controller,
required this.pageController,
required this.shrinkWrap,
this.physics,
this.padding,
this.onPageChanged,
});
/// The items to display in the [ScrollView].
final List<T> items;
/// The builder to use to render the items.
final Widget Function(BuildContext, List<T>, int) itemBuilder;
/// The builder to use to render the separator.
///
/// Only used if [viewType] is [ViewType.list].
final Widget Function(BuildContext, int) separatorBuilder;
/// Whether more [items] are being loaded.
final bool isLoading;
/// The type of [ScrollView] to use.
final ViewType viewType;
/// A [Widget] to show when more [items] are being loaded.
final Widget bottomLoader;
/// The delegate to use for the [GridView].
final SliverGridDelegate gridDelegate;
/// The options to use for the [Wrap].
final WrapOptions wrapOptions;
/// The options to use for the [PageView].
final PageOptions pageOptions;
/// The scrolling direction of the [ScrollView].
final Axis scrollDirection;
/// Whether the [ScrollView] scrolls in the reading direction.
final bool reverse;
/// The scroll controller to handle the scroll events.
final ScrollController controller;
/// The page controller to handle page view events.
final PageController pageController;
/// Should the [ScrollView] be shrink-wrapped.
final bool shrinkWrap;
/// The scroll behavior to use for the [ScrollView].
final ScrollPhysics? physics;
/// The padding to use for the [ScrollView].
final EdgeInsetsGeometry? padding;
/// Specifies what to do when page changes in the [PageView].
final void Function(int)? onPageChanged;
@override
Widget build(BuildContext context) {
switch (viewType) {
case ViewType.list:
return ListView.separated(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
itemCount: items.length + (isLoading ? 1 : 0),
itemBuilder: (BuildContext context, int index) {
if (index >= items.length) return bottomLoader;
return itemBuilder(context, items, index);
},
separatorBuilder: separatorBuilder,
);
case ViewType.grid:
return GridView.builder(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
itemCount: items.length + (isLoading ? 1 : 0),
itemBuilder: (BuildContext context, int index) {
if (index >= items.length) return bottomLoader;
return itemBuilder(context, items, index);
},
gridDelegate: gridDelegate,
);
case ViewType.wrap:
return SingleChildScrollView(
scrollDirection: scrollDirection,
reverse: reverse,
padding: padding,
physics: physics,
controller: controller,
child: Wrap(
direction: wrapOptions.direction,
alignment: wrapOptions.alignment,
spacing: wrapOptions.spacing,
runAlignment: wrapOptions.runAlignment,
runSpacing: wrapOptions.runSpacing,
crossAxisAlignment: wrapOptions.crossAxisAlignment,
textDirection: wrapOptions.textDirection,
verticalDirection: wrapOptions.verticalDirection,
clipBehavior: wrapOptions.clipBehavior,
children: List.generate(
items.length + (isLoading ? 1 : 0),
(int index) {
if (index >= items.length) return bottomLoader;
return itemBuilder(context, items, index);
},
),
),
);
case ViewType.page:
return PageView.builder(
scrollDirection: scrollDirection,
reverse: reverse,
controller: pageController,
physics: physics,
clipBehavior: pageOptions.clipBehavior,
pageSnapping: pageOptions.pageSnapping,
onPageChanged: onPageChanged,
padEnds: pageOptions.padEnds,
scrollBehavior: pageOptions.scrollBehavior,
allowImplicitScrolling: pageOptions.allowImplicitScrolling,
dragStartBehavior: pageOptions.dragStartBehavior,
itemCount: items.length + (isLoading ? 1 : 0),
itemBuilder: (BuildContext context, int index) {
if (index >= items.length) return bottomLoader;
return itemBuilder(context, items, index);
},
);
}
}
}