मैं फ़्लटर में दो फायरस्टार संग्रह से डेटा कैसे जुड़ सकता हूं?


9

मेरे पास फ़्यूचर का उपयोग करके फ़्लटर में एक चैट ऐप है, और मेरे पास दो मुख्य संग्रह हैं:

  • chatsहै, जो ऑटो आईडी पर keyed, और है message, timestampऔर uidखेतों।
  • users, जिस पर कुंजी लगाई गई है uid, और एक nameक्षेत्र है

अपने ऐप में मैं messagesइस विजेट के साथ संदेशों की एक सूची दिखाता हूं ( संग्रह से):

class ChatList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var messagesSnapshot = Firestore.instance.collection("chat").orderBy("timestamp", descending: true).snapshots();
    var streamBuilder = StreamBuilder<QuerySnapshot>(
          stream: messagesSnapshot,
          builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> querySnapshot) {
            if (querySnapshot.hasError)
              return new Text('Error: ${querySnapshot.error}');
            switch (querySnapshot.connectionState) {
              case ConnectionState.waiting: return new Text("Loading...");
              default:
                return new ListView(
                  children: querySnapshot.data.documents.map((DocumentSnapshot doc) {
                    return new ListTile(
                      title: new Text(doc['message']),
                      subtitle: new Text(DateTime.fromMillisecondsSinceEpoch(doc['timestamp']).toString()),
                    );
                  }).toList()
                );
            }
          }
        );
        return streamBuilder;
  }
}

लेकिन अब मैं usersप्रत्येक संदेश के लिए उपयोगकर्ता का नाम ( संग्रह से) दिखाना चाहता हूं ।

मैं आम तौर पर कहता हूं कि एक क्लाइंट-साइड शामिल हो, हालांकि मुझे यकीन नहीं है कि फ़्लटर का कोई विशिष्ट नाम है।

मुझे ऐसा करने का एक तरीका मिला है (जो मैंने नीचे पोस्ट किया है), लेकिन आश्चर्य है कि फ़्लटर में इस तरह के ऑपरेशन को करने के लिए एक और / बेहतर / अधिक मुहावरेदार तरीका है।

तो: उपरोक्त संरचना में प्रत्येक संदेश के लिए उपयोगकर्ता का नाम देखने के लिए फ़्लटर में मुहावरेदार तरीका क्या है?


मुझे लगता है कि एकमात्र समाधान मैंने बहुत सारे rxdart पर शोध किया है
CENk YAGMUR

जवाबों:


3

मुझे एक और संस्करण काम करने लगा जो दो नेस्टेड बिल्डरों के साथ मेरे उत्तर से थोड़ा बेहतर लगता है ।

यहां मैं एक कस्टम विधि में डेटा लोडिंग पर अलग-अलग, Messageएक संदेश Documentऔर वैकल्पिक संबद्ध उपयोगकर्ता से जानकारी रखने के लिए एक समर्पित वर्ग का उपयोग कर रहा हूं Document

class Message {
  final message;
  final timestamp;
  final uid;
  final user;
  const Message(this.message, this.timestamp, this.uid, this.user);
}
class ChatList extends StatelessWidget {
  Stream<List<Message>> getData() async* {
    var messagesStream = Firestore.instance.collection("chat").orderBy("timestamp", descending: true).snapshots();
    var messages = List<Message>();
    await for (var messagesSnapshot in messagesStream) {
      for (var messageDoc in messagesSnapshot.documents) {
        var message;
        if (messageDoc["uid"] != null) {
          var userSnapshot = await Firestore.instance.collection("users").document(messageDoc["uid"]).get();
          message = Message(messageDoc["message"], messageDoc["timestamp"], messageDoc["uid"], userSnapshot["name"]);
        }
        else {
          message = Message(messageDoc["message"], messageDoc["timestamp"], "", "");
        }
        messages.add(message);
      }
      yield messages;
    }
  }
  @override
  Widget build(BuildContext context) {
    var streamBuilder = StreamBuilder<List<Message>>(
          stream: getData(),
          builder: (BuildContext context, AsyncSnapshot<List<Message>> messagesSnapshot) {
            if (messagesSnapshot.hasError)
              return new Text('Error: ${messagesSnapshot.error}');
            switch (messagesSnapshot.connectionState) {
              case ConnectionState.waiting: return new Text("Loading...");
              default:
                return new ListView(
                  children: messagesSnapshot.data.map((Message msg) {
                    return new ListTile(
                      title: new Text(msg.message),
                      subtitle: new Text(DateTime.fromMillisecondsSinceEpoch(msg.timestamp).toString()
                                         +"\n"+(msg.user ?? msg.uid)),
                    );
                  }).toList()
                );
            }
          }
        );
        return streamBuilder;
  }
}

नेस्टेड बिल्डरों के साथ समाधान की तुलना में यह कोड अधिक पठनीय है, क्योंकि ज्यादातर डेटा हैंडलिंग और यूआई बिल्डर बेहतर अलग हो जाते हैं। यह केवल उन उपयोगकर्ताओं के लिए उपयोगकर्ता दस्तावेज़ लोड करता है जिन्होंने संदेश पोस्ट किए हैं। दुर्भाग्य से, यदि उपयोगकर्ता ने कई संदेश पोस्ट किए हैं, तो यह प्रत्येक संदेश के लिए दस्तावेज़ को लोड करेगा। मैं एक कैश जोड़ सकता हूं, लेकिन लगता है कि यह कोड पहले से ही थोड़ा लंबा है जो इसे पूरा करता है।


1
यदि आप उत्तर के रूप में "संदेश के अंदर उपयोगकर्ता जानकारी संग्रहीत करना" नहीं लेते हैं, तो मुझे लगता है कि यह सबसे अच्छा है जो आप कर सकते हैं। यदि आप उपयोगकर्ता जानकारी को संदेश के अंदर संग्रहीत करते हैं, तो यह स्पष्ट दोष है कि उपयोगकर्ता जानकारी उपयोगकर्ता संग्रह में बदल सकती है, लेकिन संदेश के अंदर नहीं। शेड्यूल्ड फायरबेस फ़ंक्शन का उपयोग करके, आप इसका समाधान भी कर सकते हैं। समय-समय पर, आप संदेश संग्रह के माध्यम से जा सकते हैं और उपयोगकर्ता संग्रह में नवीनतम डेटा के अनुसार उपयोगकर्ता जानकारी को अपडेट कर सकते हैं।
उगुरन यिल्डिरिम 13

व्यक्तिगत रूप से मैं धाराओं के संयोजन की तुलना में इस तरह से सरल समाधान पसंद करता हूं जब तक कि वास्तव में आवश्यक न हो। इससे भी बेहतर, हम इस डेटा लोडिंग पद्धति को एक सर्विस क्लास की तरह या फिर बीएलओसी पैटर्न का अनुसरण कर सकते हैं। जैसा कि आप पहले ही उल्लेख कर चुके हैं, हम उपयोगकर्ता की जानकारी को एक में सहेज सकते हैं Map<String, UserModel>और केवल एक बार उपयोगकर्ता दस्तावेज़ को लोड कर सकते हैं।
जोशुआ चान

यहोशू को सहमत किया। मुझे यह देखने में अच्छा लगेगा कि यह BLoC पैटर्न में कैसा दिखेगा।
फ्रैंक वैन पफेलन

3

अगर मैं इसे सही ढंग से पढ़ रहा हूं, तो समस्या का सार यह हो जाता है: आप डेटा की एक धारा कैसे बदलते हैं जिसके लिए स्ट्रीम में डेटा को संशोधित करने के लिए एक अतुल्यकालिक कॉल करने की आवश्यकता होती है?

समस्या के संदर्भ में, डेटा की धारा संदेशों की एक सूची है, और उपयोगकर्ता डेटा प्राप्त करने और इस डेटा के साथ संदेशों को स्ट्रीम में अद्यतन करने के लिए async कॉल है।

asyncMap()फ़ंक्शन का उपयोग करके इसे सीधे डार्ट स्ट्रीम ऑब्जेक्ट में करना संभव है । यहाँ कुछ शुद्ध डार्ट कोड है जो यह दर्शाता है कि यह कैसे करना है:

import 'dart:async';
import 'dart:math' show Random;

final random = Random();

const messageList = [
  {
    'message': 'Message 1',
    'timestamp': 1,
    'uid': 1,
  },
  {
    'message': 'Message 2',
    'timestamp': 2,
    'uid': 2,
  },
  {
    'message': 'Message 3',
    'timestamp': 3,
    'uid': 2,
  },
];

const userList = {
  1: 'User 1',
  2: 'User 2',
  3: 'User 3',
};

class Message {
  final String message;
  final int timestamp;
  final int uid;
  final String user;
  const Message(this.message, this.timestamp, this.uid, this.user);

  @override
  String toString() => '$user => $message';
}

// Mimic a stream of a list of messages
Stream<List<Map<String, dynamic>>> getServerMessagesMock() async* {
  yield messageList;
  while (true) {
    await Future.delayed(Duration(seconds: random.nextInt(3) + 1));
    yield messageList;
  }
}

// Mimic asynchronously fetching a user
Future<String> userMock(int uid) => userList.containsKey(uid)
    ? Future.delayed(
        Duration(milliseconds: 100 + random.nextInt(100)),
        () => userList[uid],
      )
    : Future.value(null);

// Transform the contents of a stream asynchronously
Stream<List<Message>> getMessagesStream() => getServerMessagesMock()
    .asyncMap<List<Message>>((messageList) => Future.wait(
          messageList.map<Future<Message>>(
            (m) async => Message(
              m['message'],
              m['timestamp'],
              m['uid'],
              await userMock(m['uid']),
            ),
          ),
        ));

void main() async {
  print('Streams with async transforms test');
  await for (var messages in getMessagesStream()) {
    messages.forEach(print);
  }
}

अधिकांश कोड संदेशों के मानचित्र की एक धारा के रूप में फायरबेस से आने वाले डेटा की नकल कर रहे हैं, और उपयोगकर्ता डेटा लाने के लिए एक async फ़ंक्शन। यहाँ महत्वपूर्ण कार्य है getMessagesStream()

कोड इस तथ्य से थोड़ा जटिल है कि यह धारा में आने वाले संदेशों की एक सूची है। उपयोगकर्ता डेटा को समान रूप से होने से रोकने के लिए कॉल को रोकने के लिए, कोड एक Future.wait()का उपयोग करता है एक इकट्ठा करने के लिए List<Future<Message>>और List<Message>जब सभी वायदा पूरा कर लिया है।

स्पंदन के संदर्भ में, आप संदेश ऑब्जेक्ट प्रदर्शित करने के लिए getMessagesStream()एक से आने वाली धारा का उपयोग कर सकते हैं FutureBuilder


3

आप इसे RxDart की तरह कर सकते हैं .. https://pub.dev/packages/rxdart

import 'package:rxdart/rxdart.dart';

class Messages {
  final String messages;
  final DateTime timestamp;
  final String uid;
  final DocumentReference reference;

  Messages.fromMap(Map<String, dynamic> map, {this.reference})
      : messages = map['messages'],
        timestamp = (map['timestamp'] as Timestamp)?.toDate(),
        uid = map['uid'];

  Messages.fromSnapshot(DocumentSnapshot snapshot)
      : this.fromMap(snapshot.data, reference: snapshot.reference);

  @override
  String toString() {
    return 'Messages{messages: $messages, timestamp: $timestamp, uid: $uid, reference: $reference}';
  }
}

class Users {
  final String name;
  final DocumentReference reference;

  Users.fromMap(Map<String, dynamic> map, {this.reference})
      : name = map['name'];

  Users.fromSnapshot(DocumentSnapshot snapshot)
      : this.fromMap(snapshot.data, reference: snapshot.reference);

  @override
  String toString() {
    return 'Users{name: $name, reference: $reference}';
  }
}

class CombineStream {
  final Messages messages;
  final Users users;

  CombineStream(this.messages, this.users);
}

Stream<List<CombineStream>> _combineStream;

@override
  void initState() {
    super.initState();
    _combineStream = Observable(Firestore.instance
        .collection('chat')
        .orderBy("timestamp", descending: true)
        .snapshots())
        .map((convert) {
      return convert.documents.map((f) {

        Stream<Messages> messages = Observable.just(f)
            .map<Messages>((document) => Messages.fromSnapshot(document));

        Stream<Users> user = Firestore.instance
            .collection("users")
            .document(f.data['uid'])
            .snapshots()
            .map<Users>((document) => Users.fromSnapshot(document));

        return Observable.combineLatest2(
            messages, user, (messages, user) => CombineStream(messages, user));
      });
    }).switchMap((observables) {
      return observables.length > 0
          ? Observable.combineLatestList(observables)
          : Observable.just([]);
    })
}

rxdart के लिए 0.23.x

@override
      void initState() {
        super.initState();
        _combineStream = Firestore.instance
            .collection('chat')
            .orderBy("timestamp", descending: true)
            .snapshots()
            .map((convert) {
          return convert.documents.map((f) {

            Stream<Messages> messages = Stream.value(f)
                .map<Messages>((document) => Messages.fromSnapshot(document));

            Stream<Users> user = Firestore.instance
                .collection("users")
                .document(f.data['uid'])
                .snapshots()
                .map<Users>((document) => Users.fromSnapshot(document));

            return Rx.combineLatest2(
                messages, user, (messages, user) => CombineStream(messages, user));
          });
        }).switchMap((observables) {
          return observables.length > 0
              ? Rx.combineLatestList(observables)
              : Stream.value([]);
        })
    }

बहुत ही शांत! क्या ज़रूरत न होने का एक तरीका है f.reference.snapshots(), क्योंकि यह अनिवार्य रूप से स्नैपशॉट को फिर से लोड कर रहा है और मैं फायरस्टार क्लाइंट पर भरोसा करने के लिए स्मार्ट नहीं होना पसंद करूंगा जो उन्हें कटौती करने के लिए पर्याप्त है (भले ही मैं लगभग निश्चित हूं कि यह कटौती करता है)।
फ्रैंक वैन पफेलन

मिल गया। इसके बजाय Stream<Messages> messages = f.reference.snapshots()..., आप कर सकते हैं Stream<Messages> messages = Observable.just(f)...। इस उत्तर के बारे में मुझे जो पसंद है वह यह है कि यह उपयोगकर्ता दस्तावेजों का अवलोकन करता है, इसलिए यदि डेटाबेस में किसी उपयोगकर्ता का नाम अपडेट किया जाता है, तो आउटपुट उस तुरंत को दर्शाता है।
फ्रैंक वैन पफेलन

हाँ इतनी अच्छी तरह से काम कर रहा हूँ कि मेरे कोड को अद्यतन कर रहा हूँ
Cenk YAGMUR

1

आदर्श रूप से आप किसी भी व्यावसायिक तर्क को अलग करना चाहते हैं जैसे डेटा लोड करना अलग सेवा में या ब्लो पैटर्न का अनुसरण करना, जैसे:

class ChatBloc {
  final Firestore firestore = Firestore.instance;
  final Map<String, String> userMap = HashMap<String, String>();

  Stream<List<Message>> get messages async* {
    final messagesStream = Firestore.instance.collection('chat').orderBy('timestamp', descending: true).snapshots();
    var messages = List<Message>();
    await for (var messagesSnapshot in messagesStream) {
      for (var messageDoc in messagesSnapshot.documents) {
        final userUid = messageDoc['uid'];
        var message;

        if (userUid != null) {
          // get user data if not in map
          if (userMap.containsKey(userUid)) {
            message = Message(messageDoc['message'], messageDoc['timestamp'], userUid, userMap[userUid]);
          } else {
            final userSnapshot = await Firestore.instance.collection('users').document(userUid).get();
            message = Message(messageDoc['message'], messageDoc['timestamp'], userUid, userSnapshot['name']);
            // add entry to map
            userMap[userUid] = userSnapshot['name'];
          }
        } else {
          message =
              Message(messageDoc['message'], messageDoc['timestamp'], '', '');
        }
        messages.add(message);
      }
      yield messages;
    }
  }
}

तब आप बस अपने कंपोनेंट में ब्लाक का इस्तेमाल कर सकते हैं और chatBloc.messagesस्ट्रीम सुन सकते हैं ।

class ChatList extends StatelessWidget {
  final ChatBloc chatBloc = ChatBloc();

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<Message>>(
        stream: chatBloc.messages,
        builder: (BuildContext context, AsyncSnapshot<List<Message>> messagesSnapshot) {
          if (messagesSnapshot.hasError)
            return new Text('Error: ${messagesSnapshot.error}');
          switch (messagesSnapshot.connectionState) {
            case ConnectionState.waiting:
              return new Text('Loading...');
            default:
              return new ListView(children: messagesSnapshot.data.map((Message msg) {
                return new ListTile(
                  title: new Text(msg.message),
                  subtitle: new Text('${msg.timestamp}\n${(msg.user ?? msg.uid)}'),
                );
              }).toList());
          }
        });
  }
}

1

मुझे RxDart समाधान के अपने संस्करण को आगे रखने की अनुमति दें। मैं का उपयोग combineLatest2एक साथ ListView.builderप्रत्येक संदेश विजेट का निर्माण। प्रत्येक संदेश के निर्माण के दौरान विजेट मैं संबंधित के साथ उपयोगकर्ता का नाम खोजता हूं uid

इस स्निपेट में मैं उपयोगकर्ता के नाम के लिए एक लीनियर लुकअप का उपयोग करता हूं, लेकिन uid -> user nameमानचित्र बनाकर इसमें सुधार किया जा सकता है

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/widgets.dart';
import 'package:rxdart/rxdart.dart';

class MessageWidget extends StatelessWidget {
  // final chatStream = Firestore.instance.collection('chat').snapshots();
  // final userStream = Firestore.instance.collection('users').snapshots();
  Stream<QuerySnapshot> chatStream;
  Stream<QuerySnapshot> userStream;

  MessageWidget(this.chatStream, this.userStream);

  @override
  Widget build(BuildContext context) {
    Observable<List<QuerySnapshot>> combinedStream = Observable.combineLatest2(
        chatStream, userStream, (messages, users) => [messages, users]);

    return StreamBuilder(
        stream: combinedStream,
        builder: (_, AsyncSnapshot<List<QuerySnapshot>> snapshots) {
          if (snapshots.hasData) {
            List<DocumentSnapshot> chats = snapshots.data[0].documents;

            // It would be more efficient to convert this list of user documents
            // to a map keyed on the uid which will allow quicker user lookup.
            List<DocumentSnapshot> users = snapshots.data[1].documents;

            return ListView.builder(itemBuilder: (_, index) {
              return Center(
                child: Column(
                  children: <Widget>[
                    Text(chats[index]['message']),
                    Text(getUserName(users, chats[index]['uid'])),
                  ],
                ),
              );
            });
          } else {
            return Text('loading...');
          }
        });
  }

  // This does a linear search through the list of users. However a map
  // could be used to make the finding of the user's name more efficient.
  String getUserName(List<DocumentSnapshot> users, String uid) {
    for (final user in users) {
      if (user['uid'] == uid) {
        return user['name'];
      }
    }
    return 'unknown';
  }
}

आर्थर को देखने के लिए बहुत अच्छा है। यह नेस्टेड बिल्डरों के साथ मेरे शुरुआती उत्तर के बहुत क्लीनर संस्करण की तरह है । निश्चित रूप से पढ़ने के लिए अधिक सरल समाधानों में से एक।
फ्रैंक वैन पफलेन

0

पहला काम जो मुझे मिला, वह है दो StreamBuilderउदाहरणों को घोंसला बनाना , प्रत्येक संग्रह / क्वेरी के लिए एक।

class ChatList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var messagesSnapshot = Firestore.instance.collection("chat").orderBy("timestamp", descending: true).snapshots();
    var usersSnapshot = Firestore.instance.collection("users").snapshots();
    var streamBuilder = StreamBuilder<QuerySnapshot>(
      stream: messagesSnapshot,
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> messagesSnapshot) {
        return StreamBuilder(
          stream: usersSnapshot,
          builder: (context, usersSnapshot) {
            if (messagesSnapshot.hasError || usersSnapshot.hasError || !usersSnapshot.hasData)
              return new Text('Error: ${messagesSnapshot.error}, ${usersSnapshot.error}');
            switch (messagesSnapshot.connectionState) {
              case ConnectionState.waiting: return new Text("Loading...");
              default:
                return new ListView(
                  children: messagesSnapshot.data.documents.map((DocumentSnapshot doc) {
                    var user = "";
                    if (doc['uid'] != null && usersSnapshot.data != null) {
                      user = doc['uid'];
                      print('Looking for user $user');
                      user = usersSnapshot.data.documents.firstWhere((userDoc) => userDoc.documentID == user).data["name"];
                    }
                    return new ListTile(
                      title: new Text(doc['message']),
                      subtitle: new Text(DateTime.fromMillisecondsSinceEpoch(doc['timestamp']).toString()
                                          +"\n"+user),
                    );
                  }).toList()
                );
            }
        });
      }
    );
    return streamBuilder;
  }
}

जैसा कि मेरे प्रश्न में कहा गया है, मुझे पता है कि यह समाधान महान नहीं है, लेकिन कम से कम यह काम करता है।

कुछ समस्याएं जो मुझे इसके साथ दिखाई देती हैं:

  • यह केवल संदेश पोस्ट करने वाले उपयोगकर्ताओं के बजाय सभी उपयोगकर्ताओं को लोड करता है। छोटे डेटा सेट में, जो एक समस्या नहीं होगी, लेकिन जैसा कि मुझे अधिक संदेश / उपयोगकर्ता मिलते हैं (और उनमें से सबसेट दिखाने के लिए क्वेरी का उपयोग करें) मैं अधिक से अधिक उपयोगकर्ताओं को लोड कर रहा हूं, जिन्होंने कोई संदेश पोस्ट नहीं किया।
  • कोड वास्तव में दो बिल्डरों के घोंसले के कारण बहुत पठनीय नहीं है। मुझे शक है कि यह मुहावरेदार स्पंदन है।

यदि आप एक बेहतर समाधान जानते हैं, तो कृपया उत्तर के रूप में पोस्ट करें।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.