使用streambuilder颤动firestore分页

Posted

技术标签:

【中文标题】使用streambuilder颤动firestore分页【英文标题】:flutter firestore pagination using streambuilder 【发布时间】:2020-03-30 04:03:37 【问题描述】:

您好,我正在尝试在列表中使用分页。并且列表数据来自firebase。我不确定如何使用流生成器对数据进行分页。现在我通过在 didChangeDependencies() 函数中调用 getdocuments 来进行分页。但添加新数据后,列表不会更新。如果有人可以提供帮助,那对我来说会很棒。这就是我现在正在做的事情..

 didChangeDependencies() 
    super.didChangeDependencies();

   getProducts();

    _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) 
        getProducts();
      
    );
  

  getProducts() async 
    if (!hasMore) 
      return;
    
    if (isLoading) 
      return;
    
    setState(() 
      isLoading = true;
    );
    QuerySnapshot querySnapshot;
    if (lastDocument == null) 
      querySnapshot = await firestore
          .collection('products')
          .limit(documentLimit)
          .orderBy('timestamp', descending: true)
          .getDocuments();
     else 
      querySnapshot = await firestore
          .collection('products')
          .startAfterDocument(lastDocument)
          .limit(documentLimit)
          .orderBy('timestamp', descending: true)
          .getDocuments();
    
    if (querySnapshot.documents.length < documentLimit) 
      hasMore = false;
    
    if (querySnapshot.documents.isNotEmpty) 
      lastDocument =
          querySnapshot.documents[querySnapshot.documents.length - 1];
      products.addAll(querySnapshot.documents);
      setState(() 
        isLoading = false;
      );
    
  

【问题讨论】:

【参考方案1】:

试试下面的代码:

class ProductList extends StatefulWidget 
  @override
  _ProductListState createState() => _ProductListState();


class _ProductListState extends State<ProductList> 
  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) 
      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 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;
    
  

产品项的 JSON:


  "index": 1,
  "name": "Pork",
  "description": "Thịt heo/lợn"

链接 Github 示例: https://github.com/simplesoft-duongdt3/flutter_firestore_paging

【讨论】:

您能否提供使用查询快照流进行分页的代码....不是文档快照列表 @SouravDas 我已经为你添加了示例小部件。请检查一下。 我想要实时更新的分页...但是您的代码仅适用于分页..但实时更新不起作用 我正在关注这个并得到了我的结果。但是,这最初不会给我在 initstate 中的产品集合带来数千次阅读吗?如果是这样,在我们阅读它们时,是否值得听听他们对 requestNextPage 方法的更改? @PabloCrucitta - 谢谢。添加 .limit(whatever_pagination_you_have)... 所以它会减少你的阅读量。这将只监听已添加的快照,而不是所有不必要的快照。【参考方案2】:

我用聊天而不是产品做了类似的事情。

看看这段代码是否有用:

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

【讨论】:

【参考方案3】:

@sourav-das,请看这段代码

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  


class MyHomePage extends StatefulWidget 
  @override
  _MyHomePageState createState() => _MyHomePageState();


class _MyHomePageState extends State<MyHomePage> 
  Firestore firestore = Firestore.instance;
  List<DocumentSnapshot> products = [];
  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();
    getProducts();
    _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) 
        getProducts();
      
    );
  

  getProducts() async 
    if (!hasMore) 
      print('No More Products');
      return;
    
    if (isLoading) 
      return;
    
    setState(() 
      isLoading = true;
    );
    QuerySnapshot querySnapshot;
    if (lastDocument == null) 
      querySnapshot = await firestore
          .collection('products')
          .orderBy('name')
          .limit(documentLimit)
          .getDocuments();
     else 
      querySnapshot = await firestore
          .collection('products')
          .orderBy('name')
          .startAfterDocument(lastDocument)
          .limit(documentLimit)
          .getDocuments();
      print(1);
    
    if (querySnapshot.documents.length < documentLimit) 
      hasMore = false;
    

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

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

    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) 
              if (snapshot.hasData && snapshot.data.length > 0) 
                return ListView.builder(
                  controller: _scrollController,
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) 
                    return ListTile(
                      contentPadding: EdgeInsets.all(5),
                      title: Text(snapshot.data[index].data['name']),
                      subtitle: Text(snapshot.data[index].data['short_desc']),
                    );
                  ,
                );
               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进行分页。所以我可以听数据的变化.. @SouravDas 请检查我是否使用 StreamBuilder 编辑了代码。 当我滚动它时,它会删除列表中的前一项,用新项替换它们。就像如果总共有 20 个项目,我添加了 10 个限制,然后在滚动前 10 个后,它会删除它们并添加新的 10 个项目......这不是正确的方法 您为变量 products 赋值,但您在哪里使用它...... streambuilder 和以前一样......如果可以,请提供正确的解决方案。谢谢 @MaulikGajjar:你们有没有发现这个问题?上面的代码对我有用,但我想在添加新文档后立即更新它(如果是聊天应用程序),而这段代码没有这样做。

以上是关于使用streambuilder颤动firestore分页的主要内容,如果未能解决你的问题,请参考以下文章

过滤 Streambuilder/listviewBuilder 颤动

在颤动中使用 StreamBuilder 时将数据传递到下一个屏幕

如何在颤动中直接将数据从 StreamBuilder 获取到 GridView 中?

在颤动中合并来自 Firestore 的流

颤动firebase检查集合是不是为空

使用 Firestore 和 Flutter 填充 DataTable(使用 StreamBuilder)