1
votes

Pagination StreamBuilder Firestore

Je suis nouveau dans le flutter, et j'essaie de paginer une discussion lorsque le défilement atteint le haut avec streambuilder. Le problème est le suivant: lorsque je fais la requête dans scrollListener streambuilder, priorise sa requête au-dessus de scrollListener et renvoie l'ancienne réponse. Est-ce qu'il y a un moyen de faire ça? Quelles sont mes options ici? Merci!

Classe ChatScreenState

Dans initState, je crée l'écouteur de défilement.

  _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"),
  ));
 }
}

Ici, je crée le StreamBuilder avec la requête limitée à 20 derniers messages. Utilisation des _messagesSnapshots comme liste globale.

@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]);
   },
 );
}

Et dans la méthode _scollListener, j'interroge les 20 messages suivants et j'ajoute le résultat à la liste globale.

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


2 commentaires

Avez-vous de la chance avec la solution?


@Purus consultez ce projet github. Fonctionne très bien github.com/simplesoft-duongdt3/flutter_firestore_paging


3 Réponses :


1
votes

Tout d'abord, je doute qu'une telle API soit le bon backend pour une application de chat avec des données en direct - Les API paginées sont mieux adaptées au contenu statique . Par exemple, à quoi fait exactement référence «page 2» si 30 messages ont été ajoutés après le chargement de «page 1»? Notez également que Firebase facture les demandes Firestore sur une base par document, de sorte que chaque message qui est demandé deux fois nuit à votre quota et à votre portefeuille .

Comme vous le voyez, une API paginée avec un la longueur de la page n'est probablement pas la bonne. C'est pourquoi je vous conseille fortement de plutôt demander des messages qui ont été envoyés dans un certain intervalle de temps. La requête Firestore peut contenir du code comme celui-ci:

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

De toute façon, voici ma réponse à une question similaire sur les API paginées dans Flutter , qui contient le code d'une implémentation réelle qui charge le nouveau contenu sous forme de ListView défile.


1 commentaires

En fait, je pense que Firestore ne facture pas les demandes de documents en double. Ils facturent 1 document lu par requête + chaque nouveau document récupéré + chaque mise à jour de document (le contenu a changé). Ils ont une limite de session + une limite de temps où ils se rechargent pour la même demande de document.



3
votes

Je vais publier mon code, j'espère que quelqu'un publiera une meilleure solution, ce n'est probablement pas la meilleure mais cela fonctionne.

Dans mon application, la solution actuelle est de changer l'état de la liste lorsque vous atteignez le sommet, arrêtez le flux et afficher les anciens messages.

Tout le code (État)

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

Les méthodes les plus importantes sont:

_scrollListener

Lorsque j'atteins le haut, j'interroge d'anciens messages et dans setState, je règle isLoading var sur true et avec les anciens messages, le tableau im va montrer.

  _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"),
//      ));
    }
  }

Et loadToTrue qui écoutent pendant que nous recherchons d'anciens messages. S'il y a un nouveau message, nous réactivons le flux.

loadToTrue

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"),
//      ));
    }
  }
}

J'espère que cela aidera tous ceux qui ont le même problème (@Purus) et attendez que quelqu'un nous donne une meilleure solution!


1 commentaires

Excellente réponse, je voudrais ajouter une meilleure pratique. Vous devez annuler toutes les instances précédentes du flux "loadToTrue". Vous pouvez le faire comme ceci: Déclarez un champ, par exemple StreamSubscription onChangeSubscription; puis enregistrez l'instance du flux dans ce fichier, et dans _scrollListener faites // Annulez l'instance précédente d'abonnement au flux loadToTrue. if (onChangeSubscription! = null) {onChangeSubscription.cancel (); } // Démarre une nouvelle instance d'abonnement au flux loadToTrue. loadToTrue ();



0
votes

J'ai un moyen de l'archiver. Désolé pour mon mauvais anglais

bool loadMoreMessage = false; int lastMessageIndex = 25 /// supposé à chaque fois faire défiler vers le haut de ListView charger plus de 25 documents Quand je défile vers le haut de ListView => setState loadMoreMessage = true;

Voici mon code: p>

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]);
                },
              ),
            );
          }
        },
      ),


0 commentaires