Mo Meshkani
Mo Meshkani

Reputation: 1684

Pagination for Firestore animated list flutter

I'm making a chat app that messages should be shown on screen with nice animation and my backend is Firestore, so I decided to use this (https://pub.dev/packages/firestore_ui) plugin for animating messages. Now I want to implement pagination to prevent expensive works and bills. Is there any way? How should I implement it? main problem is making a firestore animated list with pagination, It's easy to make simple ListView with pagination. as you can see in below code, this plugin uses Query of snapShots to show incoming messages (documents) with animation:

FirestoreAnimatedList(
    query: query,
    itemBuilder: (
        BuildContext context,
        DocumentSnapshot snapshot,
        Animation<double> animation,
        int index,
    ) => FadeTransition(
            opacity: animation,
            child: MessageListTile(
            index: index,
            document: snapshot,
            onTap: _removeMessage,
        ),
    ),
);

if we want to use AnimatedList widget instead, we will have problem because we should track realtime messages(documents) that are adding to our collection.

Upvotes: 1

Views: 1398

Answers (2)

Simon
Simon

Reputation: 11190

I put together an example for you: https://gist.github.com/slightfoot/d936391bfb77a5301335c12e3e8861de

// MIT License
//
// Copyright (c) 2020 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:provider/provider.dart';

///
/// Firestore Chat List Example - by Simon Lightfoot
///
/// Setup instructions:
///
/// 1. Create project on console.firebase.google.com.
/// 2. Add firebase_auth package to your pubspec.yaml.
/// 3. Add cloud_firestore package to your pubspec.yaml.
/// 4. Follow the steps to add firebase to your application on Android/iOS.
/// 5. Go to the authentication section of the firebase console and enable
///    anonymous auth.
///
/// Now run the example on two or more devices and start chatting.
///
///

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final user = await FirebaseAuth.instance.currentUser();
  runApp(ExampleChatApp(user: user));
}

class ExampleChatApp extends StatefulWidget {
  const ExampleChatApp({
    Key key,
    this.user,
  }) : super(key: key);

  final FirebaseUser user;

  static Future<FirebaseUser> signIn(BuildContext context, String displayName) {
    final state = context.findAncestorStateOfType<_ExampleChatAppState>();
    return state.signIn(displayName);
  }

  static Future<void> postMessage(ChatMessage message) async {
    await Firestore.instance
        .collection('messages')
        .document()
        .setData(message.toJson());
  }

  static Future<void> signOut(BuildContext context) {
    final state = context.findAncestorStateOfType<_ExampleChatAppState>();
    return state.signOut();
  }

  @override
  _ExampleChatAppState createState() => _ExampleChatAppState();
}

class _ExampleChatAppState extends State<ExampleChatApp> {
  StreamSubscription<FirebaseUser> _userSub;
  FirebaseUser _user;

  Future<FirebaseUser> signIn(String displayName) async {
    final result = await FirebaseAuth.instance.signInAnonymously();
    await result.user.updateProfile(
      UserUpdateInfo()..displayName = displayName,
    );
    final user = await FirebaseAuth.instance.currentUser();
    setState(() => _user = user);
    return user;
  }

  Future<void> signOut() {
    return FirebaseAuth.instance.signOut();
  }

  @override
  void initState() {
    super.initState();
    _user = widget.user;
    _userSub = FirebaseAuth.instance.onAuthStateChanged.listen((user) {
      print('changed ${user?.uid} -> ${user?.displayName}');
      setState(() => _user = user);
    });
  }

  @override
  void dispose() {
    _userSub.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Provider<FirebaseUser>.value(
      value: _user,
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Firestore Chat List',
        home: _user == null ? LoginScreen() : ChatScreen(),
      ),
    );
  }
}

class LoginScreen extends StatefulWidget {
  static Route<dynamic> route() {
    return MaterialPageRoute(
      builder: (BuildContext context) {
        return LoginScreen();
      },
    );
  }

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  TextEditingController _displayName;
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _displayName = TextEditingController();
  }

  @override
  void dispose() {
    _displayName.dispose();
    super.dispose();
  }

  Future<void> _onSubmitPressed() async {
    setState(() => _loading = true);
    try {
      final user = await ExampleChatApp.signIn(context, _displayName.text);
      if (mounted) {
        await ExampleChatApp.postMessage(
            ChatMessage.notice(user, 'has entered the chat'));
        Navigator.of(context).pushReplacement(ChatScreen.route());
      }
    } finally {
      if (mounted) {
        setState(() => _loading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Firestore Chat List'),
      ),
      body: SizedBox.expand(
        child: Padding(
          padding: const EdgeInsets.all(32.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                'Login',
                style: theme.textTheme.headline4,
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 32.0),
              if (_loading)
                CircularProgressIndicator()
              else ...[
                TextField(
                  controller: _displayName,
                  decoration: InputDecoration(
                    hintText: 'Display Name',
                    border: OutlineInputBorder(),
                    isDense: true,
                  ),
                  onSubmitted: (_) => _onSubmitPressed(),
                  textInputAction: TextInputAction.go,
                ),
                SizedBox(height: 12.0),
                RaisedButton(
                  onPressed: () => _onSubmitPressed(),
                  child: Text('ENTER CHAT'),
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

class ChatScreen extends StatelessWidget {
  static Route<dynamic> route() {
    return MaterialPageRoute(
      builder: (BuildContext context) {
        return ChatScreen();
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Firestore Chat List'),
        actions: [
          IconButton(
            onPressed: () async {
              final user = Provider.of<FirebaseUser>(context, listen: false);
              ExampleChatApp.postMessage(
                  ChatMessage.notice(user, 'has left the chat.'));
              Navigator.of(context).pushReplacement(LoginScreen.route());
              await ExampleChatApp.signOut(context);
            },
            icon: Icon(Icons.exit_to_app),
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: FirestoreChatList(
              listenBuilder: () {
                return Firestore.instance
                    .collection('messages')
                    .orderBy('posted', descending: true);
              },
              pagedBuilder: () {
                return Firestore.instance
                    .collection('messages')
                    .orderBy('posted', descending: true)
                    .limit(15);
              },
              itemBuilder: (BuildContext context, int index,
                  DocumentSnapshot document, Animation<double> animation) {
                final message = ChatMessage.fromDoc(document);
                return SizeTransition(
                  key: Key('message-${document.documentID}'),
                  axis: Axis.vertical,
                  axisAlignment: -1.0,
                  sizeFactor: animation,
                  child: Builder(
                    builder: (BuildContext context) {
                      switch (message.type) {
                        case ChatMessageType.notice:
                          return ChatMessageNotice(message: message);
                        case ChatMessageType.text:
                          return ChatMessageBubble(message: message);
                      }
                      throw StateError('Bad message type');
                    },
                  ),
                );
              },
            ),
          ),
          SendMessagePanel(),
        ],
      ),
    );
  }
}

class ChatMessageNotice extends StatelessWidget {
  const ChatMessageNotice({
    Key key,
    @required this.message,
  }) : super(key: key);

  final ChatMessage message;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(24.0),
      alignment: Alignment.center,
      child: Text(
        '${message.displayName} ${message.message}',
        textAlign: TextAlign.center,
        style: TextStyle(
          color: Colors.grey.shade700,
          fontStyle: FontStyle.italic,
        ),
      ),
    );
  }
}

class ChatMessageBubble extends StatelessWidget {
  const ChatMessageBubble({
    Key key,
    @required this.message,
  }) : super(key: key);

  final ChatMessage message;

  MaterialColor _calculateUserColor(String uid) {
    final hash = uid.codeUnits.fold(0, (prev, el) => prev + el);
    return Colors.primaries[hash % Colors.primaries.length];
  }

  @override
  Widget build(BuildContext context) {
    final isMine = message.isMine(context);
    return Container(
      padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
      width: double.infinity,
      child: Column(
        crossAxisAlignment:
            isMine ? CrossAxisAlignment.end : CrossAxisAlignment.start,
        children: [
          FractionallySizedBox(
            widthFactor: 0.6,
            child: Container(
              decoration: BoxDecoration(
                color: _calculateUserColor(message.uid).shade200,
                borderRadius: isMine
                    ? const BorderRadius.only(
                        topLeft: Radius.circular(24.0),
                        topRight: Radius.circular(24.0),
                        bottomLeft: Radius.circular(24.0),
                      )
                    : const BorderRadius.only(
                        topLeft: Radius.circular(24.0),
                        topRight: Radius.circular(24.0),
                        bottomRight: Radius.circular(24.0),
                      ),
              ),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  if (message.displayName?.isNotEmpty ?? false) ...[
                    const SizedBox(width: 8.0),
                    Container(
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: _calculateUserColor(message.uid),
                      ),
                      padding: EdgeInsets.all(8.0),
                      child: Text(
                        message.displayName.substring(0, 1),
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 24.0,
                        ),
                      ),
                    ),
                  ],
                  Expanded(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(message.message),
                    ),
                  ),
                ],
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 4.0),
            child: Text(
              message.infoText(context),
              style: TextStyle(
                fontSize: 12.0,
                color: Colors.grey.shade600,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class SendMessagePanel extends StatefulWidget {
  @override
  _SendMessagePanelState createState() => _SendMessagePanelState();
}

class _SendMessagePanelState extends State<SendMessagePanel> {
  final _controller = TextEditingController();

  FirebaseUser _user;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _user = Provider.of<FirebaseUser>(context);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _onSubmitPressed() {
    if (_controller.text.isEmpty) {
      return;
    }
    ExampleChatApp.postMessage(ChatMessage.text(_user, _controller.text));
    _controller.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        color: Colors.grey.shade200,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.2),
            offset: Offset(0.0, -3.0),
            blurRadius: 4.0,
            spreadRadius: 3.0,
          )
        ],
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          Expanded(
            child: ConstrainedBox(
              constraints: BoxConstraints(maxHeight: 160.0),
              child: TextField(
                controller: _controller,
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  filled: true,
                  fillColor: Colors.grey.shade300,
                  isDense: true,
                ),
                onSubmitted: (_) => _onSubmitPressed(),
                maxLines: null,
                textInputAction: TextInputAction.send,
              ),
            ),
          ),
          IconButton(
            onPressed: () => _onSubmitPressed(),
            icon: Icon(Icons.send),
          ),
        ],
      ),
    );
  }
}

enum ChatMessageType {
  notice,
  text,
}

class ChatMessage {
  const ChatMessage._({
    this.type,
    this.posted,
    this.message = '',
    this.uid,
    this.displayName,
    this.photoUrl,
  }) : assert(type != null && posted != null);

  final ChatMessageType type;
  final DateTime posted;
  final String message;
  final String uid;
  final String displayName;
  final String photoUrl;

  String infoText(BuildContext context) {
    final timeOfDay = TimeOfDay.fromDateTime(posted);
    final localizations = MaterialLocalizations.of(context);
    final date = localizations.formatShortDate(posted);
    final time = localizations.formatTimeOfDay(timeOfDay);
    return '$date at $time from $displayName';
  }

  bool isMine(BuildContext context) {
    final user = Provider.of<FirebaseUser>(context);
    return uid == user?.uid;
  }

  factory ChatMessage.notice(FirebaseUser user, String message) {
    return ChatMessage._(
      type: ChatMessageType.notice,
      posted: DateTime.now().toUtc(),
      message: message,
      uid: user.uid,
      displayName: user.displayName,
      photoUrl: user.photoUrl,
    );
  }

  factory ChatMessage.text(FirebaseUser user, String message) {
    return ChatMessage._(
      type: ChatMessageType.text,
      posted: DateTime.now().toUtc(),
      message: message,
      uid: user.uid,
      displayName: user.displayName,
      photoUrl: user.photoUrl,
    );
  }

  factory ChatMessage.fromDoc(DocumentSnapshot doc) {
    return ChatMessage._(
      type: ChatMessageType.values[doc['type'] as int],
      posted: (doc['posted'] as Timestamp).toDate(),
      message: doc['message'] as String,
      uid: doc['user']['uid'] as String,
      displayName: doc['user']['displayName'] as String,
      photoUrl: doc['user']['photoUrl'] as String,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'type': type.index,
      'posted': Timestamp.fromDate(posted),
      'message': message,
      'user': {
        'uid': uid,
        'displayName': displayName,
        'photoUrl': photoUrl,
      },
    };
  }
}

// ---- CHAT LIST IMPLEMENTATION ----

typedef Query FirestoreChatListQueryBuilder();

typedef Widget FirestoreChatListItemBuilder(
  BuildContext context,
  int index,
  DocumentSnapshot document,
  Animation<double> animation,
);

typedef Widget FirestoreChatListLoaderBuilder(
  BuildContext context,
  int index,
  Animation<double> animation,
);

class FirestoreChatList extends StatefulWidget {
  const FirestoreChatList({
    Key key,
    this.controller,
    @required this.listenBuilder,
    @required this.pagedBuilder,
    @required this.itemBuilder,
    this.loaderBuilder = defaultLoaderBuilder,
    this.scrollDirection = Axis.vertical,
    this.reverse = true,
    this.primary,
    this.physics,
    this.shrinkWrap = false,
    this.initialAnimate = false,
    this.padding,
    this.duration = const Duration(milliseconds: 300),
  }) : super(key: key);

  final FirestoreChatListQueryBuilder listenBuilder;
  final FirestoreChatListQueryBuilder pagedBuilder;
  final FirestoreChatListItemBuilder itemBuilder;
  final FirestoreChatListLoaderBuilder loaderBuilder;
  final ScrollController controller;
  final Axis scrollDirection;
  final bool reverse;
  final bool primary;
  final ScrollPhysics physics;
  final bool shrinkWrap;
  final bool initialAnimate;
  final EdgeInsetsGeometry padding;
  final Duration duration;

  static Widget defaultLoaderBuilder(
      BuildContext context, int index, Animation<double> animation) {
    return FadeTransition(
      opacity: animation,
      child: Container(
        padding: EdgeInsets.all(32.0),
        alignment: Alignment.center,
        child: CircularProgressIndicator(),
      ),
    );
  }

  @override
  _FirestoreChatListState createState() => _FirestoreChatListState();
}

class _FirestoreChatListState extends State<FirestoreChatList> {
  final _animatedListKey = GlobalKey<AnimatedListState>();
  final _dataListen = List<DocumentSnapshot>();
  final _dataPaged = List<DocumentSnapshot>();

  Future _pageRequest;
  StreamSubscription<QuerySnapshot> _listenSub;
  ScrollController _controller;

  ScrollController get controller =>
      widget.controller ?? (_controller ??= ScrollController());

  @override
  void initState() {
    super.initState();
    controller.addListener(_onScrollChanged);
    _requestNextPage();
  }

  @override
  void dispose() {
    controller.removeListener(_onScrollChanged);
    _controller?.dispose();
    _listenSub?.cancel();
    super.dispose();
  }

  void _onScrollChanged() {
    if (!controller.hasClients) {
      return;
    }
    final position = controller.position;
    if ((position.pixels >=
            (position.maxScrollExtent - position.viewportDimension)) &&
        position.userScrollDirection == ScrollDirection.reverse) {
      _requestNextPage();
    }
  }

  void _requestNextPage() {
    _pageRequest ??= () async {
      final loaderIndex = _addLoader();

      // await Future.delayed(const Duration(seconds: 3));

      var pagedQuery = widget.pagedBuilder();
      if (_dataPaged.isNotEmpty) {
        pagedQuery = pagedQuery.startAfterDocument(_dataPaged.last);
      }
      final snapshot = await pagedQuery.getDocuments();
      if (!mounted) {
        return;
      }

      final insertIndex = _dataListen.length + _dataPaged.length;
      _dataPaged.addAll(snapshot.documents);
      _removeLoader(loaderIndex);
      for (int i = 0; i < snapshot.documents.length; i++) {
        _animateAdded(insertIndex + i);
      }

      if (_listenSub == null) {
        var listenQuery = widget.listenBuilder();
        if (_dataPaged.isNotEmpty) {
          listenQuery = listenQuery.endBeforeDocument(_dataPaged.first);
        }
        _listenSub = listenQuery.snapshots().listen(_onListenChanged);
      }
      _pageRequest = null;
    }();
  }

  void _onListenChanged(QuerySnapshot snapshot) {
    for (final change in snapshot.documentChanges) {
      switch (change.type) {
        case DocumentChangeType.added:
          _dataListen.insert(change.newIndex, change.document);
          _animateAdded(change.newIndex);
          break;
        case DocumentChangeType.modified:
          if (change.oldIndex == change.newIndex) {
            _dataListen.removeAt(change.oldIndex);
            _dataListen.insert(change.newIndex, change.document);
            setState(() {});
          } else {
            final oldDoc = _dataListen.removeAt(change.oldIndex);
            _animateRemoved(change.oldIndex, oldDoc);
            _dataListen.insert(change.newIndex, change.document);
            _animateAdded(change.newIndex);
          }
          break;
        case DocumentChangeType.removed:
          final oldDoc = _dataListen.removeAt(change.oldIndex);
          _animateRemoved(change.oldIndex, oldDoc);
          break;
      }
    }
  }

  int _addLoader() {
    final index = _dataListen.length + _dataPaged.length;
    _animatedListKey?.currentState
        ?.insertItem(index, duration: widget.duration);
    return index;
  }

  void _removeLoader(int index) {
    _animatedListKey?.currentState?.removeItem(
      index,
      (BuildContext context, Animation<double> animation) {
        return widget.loaderBuilder(context, index, animation);
      },
      duration: widget.duration,
    );
  }

  void _animateAdded(int index) {
    final animatedListState = _animatedListKey.currentState;
    if (animatedListState != null) {
      animatedListState.insertItem(index, duration: widget.duration);
    } else {
      setState(() {});
    }
  }

  void _animateRemoved(int index, DocumentSnapshot old) {
    final animatedListState = _animatedListKey.currentState;
    if (animatedListState != null) {
      animatedListState.removeItem(
        index,
        (BuildContext context, Animation<double> animation) {
          return widget.itemBuilder(context, index, old, animation);
        },
        duration: widget.duration,
      );
    } else {
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_dataListen.length == 0 &&
        _dataPaged.length == 0 &&
        !widget.initialAnimate) {
      return SizedBox();
    }
    return AnimatedList(
      key: _animatedListKey,
      controller: controller,
      scrollDirection: widget.scrollDirection,
      reverse: widget.reverse,
      primary: widget.primary,
      physics: widget.physics,
      shrinkWrap: widget.shrinkWrap,
      padding: widget.padding ?? MediaQuery.of(context).padding,
      initialItemCount: _dataListen.length + _dataPaged.length,
      itemBuilder: (
        BuildContext context,
        int index,
        Animation<double> animation,
      ) {
        if (index < _dataListen.length) {
          return widget.itemBuilder(
            context,
            index,
            _dataListen[index],
            animation,
          );
        } else {
          final pagedIndex = index - _dataListen.length;
          if (pagedIndex < _dataPaged.length) {
            return widget.itemBuilder(
                context, index, _dataPaged[pagedIndex], animation);
          } else {
            return widget.loaderBuilder(
              context,
              pagedIndex,
              AlwaysStoppedAnimation<double>(1.0),
            );
          }
        }
      },
    );
  }
}

Upvotes: 2

griffins
griffins

Reputation: 8246

you can check this github project by simplesoft-duongdt3;

TLDR this is how to go about it

  StreamController<List<DocumentSnapshot>> _streamController =
  StreamController<List<DocumentSnapshot>>();
  List<DocumentSnapshot> _products = [];

  bool _isRequesting = false;
  bool _isFinish = false;

  void onChangeData(List<DocumentChange> documentChanges) {
    var isChange = false;
    documentChanges.forEach((productChange) {
      print(
          "productChange ${productChange.type.toString()} ${productChange.newIndex} ${productChange.oldIndex} ${productChange.document}");

      if (productChange.type == DocumentChangeType.removed) {
        _products.removeWhere((product) {
          return productChange.document.documentID == product.documentID;
        });
        isChange = true;
      } else {

        if (productChange.type == DocumentChangeType.modified) {
          int indexWhere = _products.indexWhere((product) {
            return productChange.document.documentID == product.documentID;
          });

          if (indexWhere >= 0) {
            _products[indexWhere] = productChange.document;
          }
          isChange = true;
        }
      }
    });

    if(isChange) {
      _streamController.add(_products);
    }
  }

  @override
  void initState() {
    Firestore.instance
        .collection('products')
        .snapshots()
        .listen((data) => onChangeData(data.documentChanges));

    requestNextPage();
    super.initState();
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scrollInfo) {
          if (scrollInfo.metrics.maxScrollExtent == scrollInfo.metrics.pixels) {
            requestNextPage();
          }
          return true;
        },
        child: StreamBuilder<List<DocumentSnapshot>>(
          stream: _streamController.stream,
          builder: (BuildContext context,
              AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
            if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
            switch (snapshot.connectionState) {
              case ConnectionState.waiting:
                return new Text('Loading...');
              default:
                log("Items: " + snapshot.data.length.toString());
                return //your grid here
                  ListView.separated(
                  separatorBuilder: (context, index) => Divider(
                    color: Colors.black,
                  ),
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) => Padding(
                    padding: const EdgeInsets.symmetric(vertical: 32),
                    child: new ListTile(
                      title: new Text(snapshot.data[index]['name']),
                      subtitle: new Text(snapshot.data[index]['description']),
                    ),
                  ),
                );
            }
          },
        ));
  }

  void requestNextPage() async {
    if (!_isRequesting && !_isFinish) {
      QuerySnapshot querySnapshot;
      _isRequesting = true;
      if (_products.isEmpty) {
        querySnapshot = await Firestore.instance
            .collection('products')
            .orderBy('index')
            .limit(5)
            .getDocuments();
      } else {
        querySnapshot = await Firestore.instance
            .collection('products')
            .orderBy('index')
            .startAfterDocument(_products[_products.length - 1])
            .limit(5)
            .getDocuments();
      }

      if (querySnapshot != null) {
        int oldSize = _products.length;
        _products.addAll(querySnapshot.documents);
        int newSize = _products.length;
        if (oldSize != newSize) {
          _streamController.add(_products);
        } else {
          _isFinish = true;
        }
      }
      _isRequesting = false;
    }
}

Upvotes: 1

Related Questions