【问题标题】:Pagination for Firestore animated list flutterFirestore 动画列表颤动的分页
【发布时间】:2020-05-06 23:25:35
【问题描述】:

我正在制作一个聊天应用程序,消息应该以漂亮的动画显示在屏幕上,我的后端是 Firestore,所以我决定使用这个 (https://pub.dev/packages/firestore_ui) 插件来为消息制作动画。 现在我想实施分页以防止昂贵的工作和账单。 有什么办法吗? 我应该如何实施? 主要问题是用pagination 制作一个firestore 动画列表, 通过分页可以轻松制作简单的ListView。 正如你在下面的代码中看到的,这个插件使用Query of snapShots 来显示带有动画的传入消息(文档):

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

如果我们想改用AnimatedList 小部件,我们会遇到问题,因为我们应该跟踪添加到我们集合中的实时消息(文档)。

【问题讨论】:

    标签: flutter google-cloud-firestore


    【解决方案1】:

    我为你整理了一个例子: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),
                );
              }
            }
          },
        );
      }
    }
    
    

    【讨论】:

      【解决方案2】:

      你可以通过 simplesoft-duongdt3 检查这个github project

      TLDR 这是怎么做的

        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;
          }
      }
      

      【讨论】:

      • 问题是我想要一个 AnimatedList 。我不知道如何将传入消息动画化到 Firestore。这就是我使用提到的插件的原因。如您所见,它使用快照查询,而不依赖于我们的 QuerySnapshots 列表。
      • 使用我的示例将您的 query 设置为 requestNextPage 。让我知道它是否有效
      • 不,它不起作用,我之前试过。方法应该不同
      猜你喜欢
      • 2020-03-30
      • 2021-06-23
      • 2021-02-20
      • 2019-01-15
      • 1970-01-01
      • 1970-01-01
      • 2020-04-28
      • 2022-01-01
      • 2021-04-28
      相关资源
      最近更新 更多