【问题标题】:StreamBuilder Firestore PaginationStreamBuilder Firestore 分页
【发布时间】:2019-05-29 23:23:11
【问题描述】:

我是 Flutter 的新手,我正在尝试在使用 streambuilder 滚动到顶部时对聊天进行分页。问题是:当我在 scrollListener 中进行查询时,streambuilder 将他的查询优先于 scrollListener 并返回旧响应。有没有办法做到这一点?我在这里有什么选择?谢谢!

类 ChatScreenState

在 initState 中我创建了滚动监听器。

  @override
void initState() {
 listScrollController = ScrollController();
 listScrollController.addListener(_scrollListener);
 super.initState();
}

在这里,我创建了 StreamBuilder,查询限制为 20 条最后的消息。使用 _messagesSnapshots 作为全局列表。

@override
Widget build(BuildContext context) {
 return Scaffold(
    key: key,
    appBar: AppBar(title: Text("Chat")),
    body: Container(
      child: Column(
        children: <Widget>[
          Flexible(
              child: StreamBuilder<QuerySnapshot>(
            stream: Firestore.instance
                .collection('messages')
                .where('room_id', isEqualTo: _roomID)
                .orderBy('timestamp', descending: true)
                .limit(20)
                .snapshots(),
            builder: (context, snapshot) {
              if (!snapshot.hasData) return LinearProgressIndicator();
              _messagesSnapshots = snapshot.data.documents;

              return _buildList(context, _messagesSnapshots);
            },
          )),
          Divider(height: 1.0),
          Container(
            decoration: BoxDecoration(color: Theme.of(context).cardColor),
            child: _buildTextComposer(),
          ),
        ],
      ),
    ));
}

Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
 _messagesSnapshots = snapshot;

 return ListView.builder(
   controller: listScrollController,
   itemCount: _messagesSnapshots.length,
   reverse: true,
   itemBuilder: (context, index) {
     return _buildListItem(context, _messagesSnapshots[index]);
   },
 );
}

在 _scollListener 方法中,我查询接下来的 20 条消息并将结果添加到全局列表中。

  _scrollListener() {

   // If reach top 
   if (listScrollController.offset >=
        listScrollController.position.maxScrollExtent &&
    !listScrollController.position.outOfRange) {

   // Then search last message
   final message = Message.fromSnapshot(
      _messagesSnapshots[_messagesSnapshots.length - 1]);

   // And get the next 20 messages from database
   Firestore.instance
      .collection('messages')
      .where('room_id', isEqualTo: _roomID)
      .where('timestamp', isLessThan: message.timestamp)
      .orderBy('timestamp', descending: true)
      .limit(20)
      .getDocuments()
      .then((snapshot) {

    // To save in the global list
    setState(() {
      _messagesSnapshots.addAll(snapshot.documents);
    });
  });

  // debug snackbar
  key.currentState.showSnackBar(new SnackBar(
    content: new Text("Top Reached"),
  ));
 }
}

【问题讨论】:

标签: firebase pagination dart flutter google-cloud-firestore


【解决方案1】:

我有办法存档。对不起我的英语不好

bool loadMoreMessage = false; int lastMessageIndex = 25 /// 假设每次滚动到 ListView 顶部加载更多 25 个文档当我滚动到 ListView 顶部时 =>setState loadMoreMessage = true;

这是我的代码:

StreamBuilder<List<Message>>(
        stream:
            _loadMoreMessage ? _streamMessage(lastMessageIndex): _streamMessage(25),
        builder: (context, AsyncSnapshot<List<Message>> snapshot) {
          if (!snapshot.hasData) {
            return Container();
          } else {
            listMessage = snapshot.data;
            return NotificationListener(
              onNotification: (notification) {
                if (notification is ScrollEndNotification) {
                  if (notification.metrics.pixels > 0) {
                    setState(() {
                      /// Logic here!
                      lastMessageIndex = lastMessageIndex + 25;
                      _loadMoreMessage = true;
                    });
                  }
                }
              },
              child: ListView.builder(
                controller: _scrollController,
                reverse: true,
                itemCount: snapshot.data.length,
                itemBuilder: (context, index) {
                  return ChatContent(listMessage[index]);
                },
              ),
            );
          }
        },
      ),

【讨论】:

    【解决方案2】:

    我要发布我的代码,希望有人发布更好的解决方案,可能不是最好的,但它有效。

    在我的应用中,实际的解决方案是在到达顶部时更改列表的状态,停止流并显示旧消息。

    所有代码(状态)

    class _MessageListState extends State<MessageList> {
      List<DocumentSnapshot> _messagesSnapshots;
      bool _isLoading = false;
    
      final TextEditingController _textController = TextEditingController();
      ScrollController listScrollController;
      Message lastMessage;
      Room room;
    
      @override
      void initState() {
        listScrollController = ScrollController();
        listScrollController.addListener(_scrollListener);
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        room = widget.room;
        return Flexible(
          child: StreamBuilder<QuerySnapshot>(
            stream: _isLoading
                ? null
                : Firestore.instance
                    .collection('rooms')
                    .document(room.id)
                    .collection('messages')
                    .orderBy('timestamp', descending: true)
                    .limit(20)
                    .snapshots(),
            builder: (context, snapshot) {
              if (!snapshot.hasData) return LinearProgressIndicator();
              _messagesSnapshots = snapshot.data.documents;
              return _buildList(context, _messagesSnapshots);
            },
          ),
        );
      }
    
      Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
        _messagesSnapshots = snapshot;
    
        if (snapshot.isNotEmpty) lastMessage = Message.fromSnapshot(snapshot[0]);
    
        return ListView.builder(
          padding: EdgeInsets.all(10),
          controller: listScrollController,
          itemCount: _messagesSnapshots.length,
          reverse: true,
          itemBuilder: (context, index) {
            return _buildListItem(context, _messagesSnapshots[index]);
          },
        );
      }
    
      Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
        final message = Message.fromSnapshot(data);
        Widget chatMessage = message.sender != widget.me.id
            ? Bubble(
                message: message,
                isMe: false,
              )
            : Bubble(
                message: message,
                isMe: true,
              );
        return Column(
          children: <Widget>[chatMessage],
        );
      }
    
      loadToTrue() {
        _isLoading = true;
        Firestore.instance
            .collection('messages')
            .reference()
            .where('room_id', isEqualTo: widget.room.id)
            .orderBy('timestamp', descending: true)
            .limit(1)
            .snapshots()
            .listen((onData) {
          print("Something change");
          if (onData.documents[0] != null) {
            Message result = Message.fromSnapshot(onData.documents[0]);
            // Here i check if last array message is the last of the FireStore DB
            int equal = lastMessage?.compareTo(result) ?? 1;
            if (equal != 0) {
              setState(() {
                _isLoading = false;
              });
            }
          }
        });
      }
    
      _scrollListener() {
        // if _scroll reach top 
        if (listScrollController.offset >=
                listScrollController.position.maxScrollExtent &&
            !listScrollController.position.outOfRange) {
          final message = Message.fromSnapshot(
              _messagesSnapshots[_messagesSnapshots.length - 1]);
          // Query old messages
          Firestore.instance
              .collection('rooms')
              .document(widget.room.id)
              .collection('messages')
              .where('timestamp', isLessThan: message.timestamp)
              .orderBy('timestamp', descending: true)
              .limit(20)
              .getDocuments()
              .then((snapshot) {
            setState(() {
              loadToTrue();
              // And add to the list
              _messagesSnapshots.addAll(snapshot.documents);
            });
          });
          // For debug purposes
    //      key.currentState.showSnackBar(new SnackBar(
    //        content: new Text("Top reached"),
    //      ));
        }
      }
    }
    

    最重要的方法是:

    _scrollListener

    当到达顶部时,我查询旧消息并在 setState 中将 isLoading var 设置为 true,并使用旧消息设置我要显示的数组。

      _scrollListener() {
        // if _scroll reach top
        if (listScrollController.offset >=
                listScrollController.position.maxScrollExtent &&
            !listScrollController.position.outOfRange) {
          final message = Message.fromSnapshot(
              _messagesSnapshots[_messagesSnapshots.length - 1]);
          // Query old messages
          Firestore.instance
              .collection('rooms')
              .document(widget.room.id)
              .collection('messages')
              .where('timestamp', isLessThan: message.timestamp)
              .orderBy('timestamp', descending: true)
              .limit(20)
              .getDocuments()
              .then((snapshot) {
            setState(() {
              loadToTrue();
              // And add to the list
              _messagesSnapshots.addAll(snapshot.documents);
            });
          });
          // For debug purposes
    //      key.currentState.showSnackBar(new SnackBar(
    //        content: new Text("Top reached"),
    //      ));
        }
      }
    

    而 loadToTrue 在我们查找旧消息时会监听。如果有新消息,我们会重新激活流。

    loadToTrue

      loadToTrue() {
        _isLoading = true;
        Firestore.instance
            .collection('rooms')
            .document(widget.room.id)
            .collection('messages')
            .orderBy('timestamp', descending: true)
            .limit(1)
            .snapshots()
            .listen((onData) {
          print("Something change");
          if (onData.documents[0] != null) {
            Message result = Message.fromSnapshot(onData.documents[0]);
            // Here i check if last array message is the last of the FireStore DB
            int equal = lastMessage?.compareTo(result) ?? 1;
            if (equal != 0) {
              setState(() {
                _isLoading = false;
              });
            }
          }
        });
      }
    

    我希望这可以帮助遇到同样问题的任何人 (@Purus),并等到有人给我们一个更好的解决方案!

    【讨论】:

    • 很好的答案,我想添加一个更好的做法。您应该取消任何以前的“loadToTrue”流实例。您可以这样做:声明一个字段,例如 StreamSubscription&lt;QuerySnapshot&gt; onChangeSubscription;,然后将流的实例保存到该字段,并在 _scrollListener 内部执行 // Cancel previous instance of subscription to the loadToTrue stream. if (onChangeSubscription != null) { onChangeSubscription.cancel(); } // Start a new instance of subscription to the loadToTrue stream. loadToTrue();
    【解决方案3】:

    首先,我怀疑这样的 API 是否适合带有实时数据的聊天应用程序的后端 - 分页 API 更适合静态内容。 例如,如果在“第 1 页”加载后添加了 30 条消息,“第 2 页”究竟指的是什么? 另请注意,Firebase 会按每个文档对 Firestore 请求收费,因此每条请求两次的消息都会损害您的配额和您的钱包

    如您所见,具有固定页面长度的分页 API 可能不合适。这就是为什么我强烈建议您宁愿请求在特定时间间隔内发送的消息。 Firestore 请求可能包含如下代码:

    .where("time", ">", lastCheck).where("time", "<=", DateTime.now())
    

    不管怎样,这里是my answer to a similar question about paginated APIs in Flutter,它包含一个实际实现的代码,该实现将新内容作为ListView滚动加载。

    【讨论】:

    • 实际上,我相信 Firestore 不会对重复的文档请求收费。他们对每个查询读取 1 个文档 + 每个获取的新文档 + 每个文档更新(内容已更改)收费。他们有会话限制 + 为相同的文档请求充电的时间限制。
    猜你喜欢
    • 2020-05-28
    • 2020-03-30
    • 2020-10-01
    • 1970-01-01
    • 2021-03-03
    • 2020-11-29
    • 2020-06-07
    • 1970-01-01
    • 2019-06-16
    相关资源
    最近更新 更多