如何在 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 为键,具有 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
集合)。
我通常称其为客户端连接,尽管我不确定 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<Messages> messages = f.reference.snapshots()...
,您可以使用Stream<Messages> 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<String, UserModel>
中,并且只加载一次用户文档。
同意约书亚。我很想看到有关这在 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<Future<Message>>
,并在所有Future 完成后创建List<Message>
。
在 Flutter 的上下文中,您可以在 FutureBuilder
中使用来自 getMessagesStream()
的流来显示 Message 对象。
【讨论】:
【参考方案4】:请允许我提出我的 RxDart 解决方案版本。我使用combineLatest2
和ListView.builder
来构建每个消息小部件。在构建每个消息小部件的过程中,我使用相应的uid
查找用户名。
在这个 sn-p 中,我使用线性查找用户名,但可以通过创建 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';
【讨论】:
很高兴见到亚瑟。这就像我最初的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
。可以在另一层中组合,并且只公开一个Future
或Stream
。
酷。如果您可以将其写在答案中,并展示如何做示例的代码,其他人可以对此表示赞同。以上是关于如何在 Flutter 中加入来自两个 Firestore 集合的数据?的主要内容,如果未能解决你的问题,请参考以下文章
flutter中的dio包如何在这段代码中加入base-url和url和apikey