web-dev-qa-db-fra.com

Combinez les flux de Firestore en flottement

J'ai essayé d'écouter plus d'une collection de Firestone en utilisant un StreamBuilder ou quelque chose de similaire. Mon code d'origine lorsque je travaillais avec un seul Stream était:

import 'package:flutter/cupertino.Dart';
import 'package:flutter/material.Dart';
import 'package:cloud_firestore/cloud_firestore.Dart';

class List extends StatefulWidget{

  ///The reference to the collection is like
  ///Firestore.instance.collection("users").document(firebaseUser.uid).collection("list1").reference()
  final CollectionReference listReference;

  List(this.listReference);

  @override
  State createState() => new ListState();
}

class ListState extends State<List> {

  @override
  Widget build(BuildContext context){

    return new StreamBuilder(
        stream: widget.listReference.snapshots(),
        builder: (context, snapshot) {
          return new ListView.builder(
              itemCount: snapshot.data.documents.length,
              padding: const EdgeInsets.only(top: 2.0),
              itemExtent: 130.0,
              itemBuilder: (context, index) {
                DocumentSnapshot ds = snapshot.data.documents[index];
                return new Data(ds);
              }
          );
        });
  }
}

Ce code fonctionne bien, mais maintenant je veux écouter plus d'une collection. Je suis tombé sur une solution qui n'implique pas un StreamBuilder et fonctionne avec une liste dynamique. Mon code ressemble maintenant à ceci:

import 'Dart:async';
import 'package:flutter/cupertino.Dart';
import 'package:flutter/material.Dart';
import 'package:cloud_firestore/cloud_firestore.Dart';
import 'main.Dart';
import 'package:async/async.Dart';

class ListHandler extends StatefulWidget{

  final CollectionReference listReference;

  ListHandler(this.listReference);

  @override
  State createState() => new ListHandlerState();
}

class ListHandlerState extends State<ListHandler> {

  StreamController streamController;
  List<dynamic> dataList = [];

  @override
  void initState() {
    streamController = StreamController.broadcast();
    setupData();
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    streamController?.close();
    streamController = null;
  }

  Future<Stream> getData() async{
      Stream stream1 = Firestore.instance.collection("users").document(firebaseUser.uid).collection("list1").snapshots();
      Stream stream2 = Firestore.instance.collection("users").document(firebaseUser.uid).collection("list2").snapshots();

      return StreamZip(([stream1, stream2])).asBroadcastStream();
  }

  setupData() async {
    Stream stream = await getData()..asBroadcastStream();
    stream.listen((snapshot) {
      setState(() {
        //Empty the list to avoid repetitions when the users updates the 
        //data in the snapshot
        dataList =[];
        List<DocumentSnapshot> list;
        for(int i=0; i < snapshot.length; i++){
          list = snapshot[i].documents;
          for (var item in list){
            dataList.add(item);
          }
        }
      });
    });
  }

  @override
  Widget build(BuildContext context){
    if(dataList.length == 0){
      return new Text("No data found");
    }

    return new ListView.builder(
        itemCount: dataList.length,
        padding: const EdgeInsets.only(top: 2.0),
        itemBuilder: (context, index) {
          DocumentSnapshot ds = dataList[index];
          return new Data(ds['title']);
        }
    );
  }
}

Le fait est que le ListView renvoie Data qui est un StatefulWidget et que l'utilisateur peut interagir avec lui en modifiant les données dans Firestore en faisant apparaître l'erreur suivante:

[VERBOSE-2:Dart_error.cc(16)] Unhandled exception:
setState() called after dispose(): ListHandlerState#81967(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().

L'application ne plante pas et fait ce qui est attendu, mais elle affiche toujours cette erreur.

Certaines personnes utilisent la bibliothèque rxdart pour travailler avec les flux et j'ai essayé de faire quelque chose comme le code ci-dessous mais quand je l'ai mis dans StreamBuilder, seuls les éléments de l'un des:

import 'Dart:async';
import 'package:flutter/cupertino.Dart';
import 'package:flutter/material.Dart';
import 'package:cloud_firestore/cloud_firestore.Dart';
import 'main.Dart';
import 'showInfo.Dart';
import 'package:rxdart/rxdart.Dart';

class ListHandler extends StatefulWidget{

  @override
  State createState() => new ListHandlerState();
}

class ListHandlerState extends State<ListHandler> {

  Stream getData() {
    Stream stream1 = Firestore.instance.collection("users").document(firebaseUser.uid).collection("list1").snapshots();
    Stream stream2 = Firestore.instance.collection("users").document(firebaseUser.uid).collection("list2").snapshots();

    return Observable.merge(([stream2, stream1]));
  }

  @override
  Widget build(BuildContext context){
    return new StreamBuilder(
        stream: getData(),
        builder: (context, snapshot) {
          if(!snapshot.hasData){
            print(snapshot);
            return new Text("loading");
          }
          return new ListView.builder(
              itemCount: snapshot.data.documents.length,
              padding: const EdgeInsets.only(top: 2.0),
              itemBuilder: (context, index) {
                DocumentSnapshot ds = snapshot.data.documents[index];
                return new Data(ds);
              }
          );
        });
  }
}

C'est la première fois que je travaille avec Streams et je ne les comprends pas très bien et j'aimerais avoir votre avis sur ce que vous devez faire.

6
Roger Bosch Mateo

Le problème ne réside pas dans la fusion, mais dans StreamBuilder mettant à jour l'interface utilisateur sur la base du dernier instantané, en d'autres termes, il n'empile pas les instantanés, il récupère simplement le dernier événement émis, en d'autres termes, les flux sont fusionnés et fusionnés flux contient les données de tous les flux fusionnés, mais streamBuilder n'affichera que le dernier événement émis par le dernier flux, voici une solution:

StreamBuilder<List<QuerySnapshot>>(stream: streamGroup, builder: (BuildContext context, 
    AsyncSnapshot<List<QuerySnapshot>> snapshotList){
                  if(!snapshotList.hasData){
                    return MyLoadingWidget();
                  }
                  // note that snapshotList.data is the actual list of querysnapshots, snapshotList alone is just an AsyncSnapshot

                  int lengthOfDocs=0;
                  int querySnapShotCounter = 0;
                  snapshotList.data.forEach((snap){lengthOfDocs = lengthOfDocs + snap.documents.length;});
                  int counter = 0;
                  return ListView.builder(
                    itemCount: lengthOfDocs,
                    itemBuilder: (_,int index){
                      try{DocumentSnapshot doc = snapshotList.data[querySnapShotCounter].documents[counter];
                      counter = counter + 1 ;
                       return new Container(child: Text(doc.data["name"]));
                      }
                      catch(RangeError){
                        querySnapShotCounter = querySnapShotCounter+1;
                        counter = 0;
                        DocumentSnapshot doc = snapshotList.data[querySnapShotCounter].documents[counter];
                        counter = counter + 1 ;
                         return new Container(child: Text(doc.data["name"]));
                      }

                    },
                  );
                },
4
Mohammed

Le dernier exemple devrait fonctionner. Peut-être que le 2e flux n'a émis aucune valeur pendant la période où vous avez observé le flux.

StreamGroup.merge() du package async devrait également fonctionner.

StreamZip crée des paires de valeurs une de chaque flux. Quand ils émettent des valeurs à un taux différent, alors un flux attend avec émission jusqu'à ce que l'autre émette une valeur. Ce n'est probablement pas ce que vous voulez.

4
Günter Zöchbauer