Flutter StreamBuilder 与 Firestore 分页

Posted

技术标签:

【中文标题】Flutter StreamBuilder 与 Firestore 分页【英文标题】:Flutter StreamBuilder with Firestore Pagination 【发布时间】:2020-05-28 20:36:54 【问题描述】:

我正在尝试构建一个基本的聊天功能,其中所有用户聊天消息都作为文档存储在“聊天”集合中。我已经成功实现了分页,以确保在用户滚动之前不会过度提取数据。

但是,即使我有一个 StreamBuilder,新的聊天文档也不会像往常一样自动出现。为什么流构建器没有注册和显示这些新消息?

这是我的代码:

class MotivatorChat extends StatefulWidget 
  @override
  _MotivatorChatState createState() => _MotivatorChatState();


class _MotivatorChatState extends State<MotivatorChat> 

  Firestore firestore = Firestore.instance;
  List<DocumentSnapshot> chats = [];
  bool isLoading = false;
  bool hasMore = true;
  int documentLimit = 10;
  DocumentSnapshot lastDocument;
  ScrollController _scrollController = ScrollController();

  StreamController<List<DocumentSnapshot>> _controller = StreamController<List<DocumentSnapshot>>();

  Stream<List<DocumentSnapshot>> get _streamController => _controller.stream;


  @override
  void initState() 
    super.initState();
    getChats();
    _scrollController.addListener(() 
      double maxScroll = _scrollController.position.maxScrollExtent;
      double currentScroll = _scrollController.position.pixels;
      double delta = MediaQuery.of(context).size.height * 0.20;
      if (maxScroll - currentScroll <= delta) 
        getChats();
      


    );
  

  getChats() async 
    if (!hasMore) 
      print('No More Chats');
      return;
    
    if (isLoading) 
      return;
    
    setState(() 
      isLoading = true;
    );
    QuerySnapshot querySnapshot;
    if (lastDocument == null) 
      querySnapshot = await firestore
          .collection('chats')
          .orderBy('timestamp', descending: true)
          .limit(documentLimit)
          .getDocuments();
     else 
      querySnapshot = await firestore
          .collection('chats')
          .orderBy('timestamp', descending: true)
          .startAfterDocument(lastDocument)
          .limit(documentLimit)
          .getDocuments();
      print(1);
    
    if (querySnapshot.documents.length < documentLimit) 
      hasMore = false;
    

    lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];

    chats.addAll(querySnapshot.documents);
    _controller.sink.add(chats);

    setState(() 
      isLoading = false;
    );
  

  @override
  Widget build(BuildContext context) 



    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Pagination with Firestore'),
      ),
      body: Column(children: [
        Expanded(
          child: StreamBuilder<List<DocumentSnapshot>>(
            stream: _streamController,
            builder: (sContext, snapshot) 
              print(snapshot.connectionState);
              if (snapshot.hasData && snapshot.data.length > 0) 
                return ListView.builder(
                  reverse: true,
                  controller: _scrollController,
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) 
                    return Padding(
                      padding: EdgeInsets.only(top: 20),
                      child: Container(
                        height: 20,
                        child: Text(snapshot.data[index].data['text']),
                      ),
                    );
                  ,
                );
               else 
                return Center(
                  child: Text('No Data...'),
                );
              
            ,
          ),
        ),
        isLoading
            ? Container(
          width: MediaQuery
              .of(context)
              .size
              .width,
          padding: EdgeInsets.all(5),
          color: Colors.yellowAccent,
          child: Text(
            'Loading',
            textAlign: TextAlign.center,
            style: TextStyle(
              fontWeight: FontWeight.bold,
            ),
          ),
        )
            : Container(),

      ]),
    );
  

更新了 StreamBuilder

StreamBuilder<List<DocumentSnapshot>>(
            stream: _streamController,
            builder: (sContext, snapshot) 
              if (snapshot.connectionState == ConnectionState.none) 
                return Text("None");
               else if (snapshot.connectionState == ConnectionState.waiting) 
                return Text("Loading");
               else if (snapshot.connectionState == ConnectionState.active) 
                if (snapshot.hasData && snapshot.data.length > 0) 
                  return ListView.builder(
                    reverse: true,
                    controller: _scrollController,
                    itemCount: snapshot.data.length,
                    itemBuilder: (context, index) 
                      return Padding(
                        padding: EdgeInsets.only(top: 20),
                        child: Container(
                          height: 20,
                          child: Text(snapshot.data[index].data['text']),
                        ),
                      );
                    ,
                  );
                 else 
                  return Center(
                    child: Text('No Data...'),
                  );
                
               else 
                return Text("return list");
              
            ,
          ),

【问题讨论】:

使用snapshot.connectionState查看。 @JohnJoe 连接状态是活动的,但仍未触发流添加新消息。 请粘贴您的最新代码。 @JohnJoe 更新了! 我没有看到你在某处使用connectionState.active 【参考方案1】:

看看这段代码是否有用:

class _MessagesState extends State<Messages> 
  ScrollController _scrollController = ScrollController();

  @override
  void initState() 
    super.initState();
    _scrollController.addListener(() 
      if (_scrollController.offset >=
              (_scrollController.position.maxScrollExtent) &&
          !_scrollController.position.outOfRange) 
        _getChats();
      
    );
  

  final StreamController<List<DocumentSnapshot>> _chatController =
      StreamController<List<DocumentSnapshot>>.broadcast();

  List<List<DocumentSnapshot>> _allPagedResults = [<DocumentSnapshot>[]];

  static const int chatLimit = 10;
  DocumentSnapshot? _lastDocument;
  bool _hasMoreData = true;

  Stream<List<DocumentSnapshot>> listenToChatsRealTime() 
    _getChats();
    return _chatController.stream;
  

  void _getChats() 
    final CollectionReference _chatCollectionReference = FirebaseFirestore
        .instance
        .collection("ChatRoom")
        .doc(widget.chatRoomId)
        .collection("channel");
    var pagechatQuery = _chatCollectionReference
        .orderBy('createdAt', descending: true)
        .limit(chatLimit);

    if (_lastDocument != null) 
      pagechatQuery = pagechatQuery.startAfterDocument(_lastDocument!);
    

    if (!_hasMoreData) return;

    var currentRequestIndex = _allPagedResults.length;
    pagechatQuery.snapshots().listen(
      (snapshot) 
        if (snapshot.docs.isNotEmpty) 
          var generalChats = snapshot.docs.toList();

          var pageExists = currentRequestIndex < _allPagedResults.length;

          if (pageExists) 
            _allPagedResults[currentRequestIndex] = generalChats;
           else 
            _allPagedResults.add(generalChats);
          

          var allChats = _allPagedResults.fold<List<DocumentSnapshot>>(
              <DocumentSnapshot>[],
              (initialValue, pageItems) => initialValue..addAll(pageItems));

          _chatController.add(allChats);

          if (currentRequestIndex == _allPagedResults.length - 1) 
            _lastDocument = snapshot.docs.last;
          

          _hasMoreData = generalChats.length == chatLimit;
        
      ,
    );
  

  @override
  Widget build(BuildContext context) 
    return Container(
      child: StreamBuilder<List<DocumentSnapshot>>(
          stream: listenToChatsRealTime(),
          builder: (ctx, chatSnapshot) 
            if (chatSnapshot.connectionState == ConnectionState.waiting ||
                chatSnapshot.connectionState == ConnectionState.none) 
              return chatSnapshot.hasData
                  ? Center(
                      child: CircularProgressIndicator(),
                    )
                  : Center(
                      child: Text("Start a conversation."),
                    );
             else 
              if (chatSnapshot.hasData) 
                final chatDocs = chatSnapshot.data!;
                final user = Provider.of<User?>(context);
                return ListView.builder(
                  controller: _scrollController,
                  reverse: true,
                  itemBuilder: (ctx, i) 
                    Map chatData = chatDocs[i].data() as Map;
                    return MessageBubble(
                        username: chatData['username'],
                        message: chatData['text'],
                        isMe: chatData['senderId'] == user!.uid,
                        key: ValueKey(chatDocs[i].id));
                  ,
                  itemCount: chatDocs.length,
                );
               else 
                return CircularProgressIndicator();
              
            
          ),
    );
  

我参考了这个答案:Pagination in Flutter with Firebase Realtime Database

【讨论】:

【参考方案2】:

如果hasMore 为假,则流不会接收新数据。

不要检查 hasMore

//     if (!hasMore) 
//       print('No More Chats');
//       return;
//     

并检查文件

//     if (querySnapshot.documents.length < documentLimit) 
//       hasMore = false;
//     

    if (querySnapshot.documents.isEmpty) 
      print('No More Chats');
      setLoading(false);
      return;
    

我在DartPad上测试,它每3秒发送一次数据。

【讨论】:

问题不在于滚动。即使不滚动,流构建器如何继续收听流并添加其他聊天?有没有办法收听流并添加到列表中? 问题是你的滚动逻辑有错误。如果hasMore 为假,则流不会接收新数据。 你可以在 DartPad 上试试这个例子。它每 3 秒接收一次数据。 dartpad.dartlang.org/965e9634d2a9fa2e64db0556d9c9bc8c【参考方案3】:

我尝试了您的代码并将文档限制设置为 20,它工作正常。

DartPad 上的示例

【讨论】:

检索数据不是问题。挑战在于它何时更新并将新文档添加到 Firestore。我希望它自动更新,但此解决方案不适用于此 因为当一个新文档被添加到 Firestore 时,hasMore 总是 false ... @PJQuakJag 你设法解决了吗?我有同样的问题

以上是关于Flutter StreamBuilder 与 Firestore 分页的主要内容,如果未能解决你的问题,请参考以下文章

Flutter StreamBuilder 与 Firestore 分页

Flutter:如何将 StreamBuilder 与 ListView.separated 一起使用

Flutter StreamBuilder 与 initialData 和 null-awareness

FirebaseStorage + Flutter,streamBuilder?

来自一个 StreamBuilder 的 Flutter 快照显示在另一个 StreamBuilder 中

Flutter:Streambuilder - 关闭流