如何在 Flutter 中加入来自两个 Firestore 集合的数据?

Posted

技术标签:

【中文标题】如何在 Flutter 中加入来自两个 Firestore 集合的数据?【英文标题】:How do I join data from two Firestore collections in Flutter? 【发布时间】:2020-03-22 11:30:55 【问题描述】:

我在 Flutter 中有一个使用 Firestore 的聊天应用,我有两个主要集合:

chats,以自动 ID 为键,具有 messagetimestampuid 字段。 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 集合)。

我通常称其为客户端连接,尽管我不确定 Flutter 是否有特定的名称。

我找到了一种方法来做到这一点(我已经在下面发布了),但想知道在 Flutter 中是否有另一种/更好/更惯用的方法来执行这种类型的操作。

那么:在 Flutter 中查找上述结构中每条消息的用户名的惯用方式是什么?

【问题讨论】:

我认为唯一的解决方案我研究了很多rxdart 【参考方案1】:

你可以像这样用 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 'Messagesmessages: $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 'Usersname: $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(),因为这本质上是重新加载快照,我不希望依靠 Firestore 客户端足够聪明来删除那些重复数据(尽管我几乎可以肯定它确实会重复数据删除)。跨度> 找到了。代替Stream&lt;Messages&gt; messages = f.reference.snapshots()...,您可以使用Stream&lt;Messages&gt; messages = Observable.just(f)...。我喜欢这个答案的地方在于它会观察用户文档,因此如果在数据库中更新了用户名,输出会立即反映这一点。 @CenkYAGMUR 你能回答这个同样的问题,但我想从 Flutter 的另一个集合中检索整个“X”集合字段***.com/q/69335293/13984728【参考方案2】:

我得到了另一个版本,它似乎比 my answer with the two nested builders 稍微好一点。

这里我隔离了自定义方法中的数据加载,使用专用的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;
  

与solution with nested builders 相比,此代码更具可读性,主要是因为数据处理和 UI 构建器分离得更好。它还仅为已发布消息的用户加载用户文档。不幸的是,如果用户发布了多条消息,它将为每条消息加载文档。我可以添加一个缓存,但认为这段代码已经有点长了。

【讨论】:

如果你不把“在消息中存储用户信息”作为答案,我认为这是你能做的最好的。如果您将用户信息存储在消息中,则存在一个明显的缺点,即用户信息可能会在用户集合中更改,但不会在消息中更改。使用预定的 firebase 功能,您也可以解决此问题。不时地,您可以通过消息收集并根据用户集合中的最新数据更新用户信息。 我个人更喜欢像这样更简单的解决方案,而不是组合流,除非真的有必要。更好的是,我们可以将此数据加载方法重构为服务类或遵循 BLoC 模式。正如您已经提到的,我们可以将用户信息保存在Map&lt;String, UserModel&gt; 中,并且只加载一次用户文档。 同意约书亚。我很想看到有关这在 BLoC 模式中的外观的文章。【参考方案3】:

如果我没看错的话,问题抽象为:如何转换需要异步调用来修改流中数据的数据流?

在问题的上下文中,数据流是消息列表,异步调用是获取用户数据并使用流中的数据更新消息。

可以使用asyncMap() 函数直接在 Dart 流对象中执行此操作。下面是一些纯 Dart 代码来演示如何做到这一点:

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

大部分代码都将来自 Firebase 的数据模拟为消息映射流,以及用于获取用户数据的异步函数。这里重要的函数是getMessagesStream()

代码稍微复杂一点,因为它是一个流中的消息列表。为了防止获取用户数据的调用同步发生,代码使用Future.wait() 收集List&lt;Future&lt;Message&gt;&gt;,并在所有Future 完成后创建List&lt;Message&gt;

在 Flutter 的上下文中,您可以在 FutureBuilder 中使用来自 getMessagesStream() 的流来显示 Message 对象。

【讨论】:

【参考方案4】:

请允许我提出我的 RxDart 解决方案版本。我使用combineLatest2ListView.builder 来构建每个消息小部件。在构建每个消息小部件的过程中,我使用相应的uid 查找用户名。

在这个 sn-p 中,我使用线性查找用户名,但可以通过创建 uid -&gt; 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';
  

【讨论】:

很高兴见到亚瑟。这就像我最初的answer with nested builders 的一个更干净的版本。绝对是更简单的阅读解决方案之一。【参考方案5】:

理想情况下,您希望排除任何业务逻辑,例如将数据加载到单独的服务中或遵循 BloC 模式,例如:

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;
    
  

然后您可以在组件中使用 Bloc 并收听 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());
          
        );
  

【讨论】:

【参考方案6】:

我得到的第一个解决方案是嵌套两个 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;
  

正如我的问题所述,我知道这个解决方案不是很好,但至少它有效。

我看到的一些问题:

它加载所有用户,而不仅仅是发布消息的用户。在小型数据集中这不会成为问题,但是随着我收到更多消息/用户(并使用查询来显示其中的一部分),我将加载越来越多没有发布任何消息的用户。 由于嵌套了两个构建器,代码的可读性并不高。我怀疑这是惯用的 Flutter。

如果您知道更好的解决方案,请发布作为答案。

【讨论】:

不需要添加到StreamBuilder。可以在另一层中组合,并且只公开一个FutureStream 酷。如果您可以将其写在答案中,并展示如何做示例的代码,其他人可以对此表示赞同。

以上是关于如何在 Flutter 中加入来自两个 Firestore 集合的数据?的主要内容,如果未能解决你的问题,请参考以下文章

flutter中的dio包如何在这段代码中加入base-url和url和apikey

json - 如何在 flutter 中的List String中加入2 json值?

如何在 Laravel 中加入来自不同主机的多个数据库

如何在 R 中加入来自 2 个不同 csv 文件的数据?

Kibana:在表格可视化中加入两个文档

如何在查询中加入 MS-SQL 和 MySQL 表?