Làm cách nào để tôi tham gia dữ liệu từ hai bộ sưu tập Firestore trong Flutter?


9

Tôi có một ứng dụng trò chuyện trong Flutter bằng Firestore và tôi có hai bộ sưu tập chính:

  • chats, Được keyed trên auto-id, và có message, timestampuidcác lĩnh vực.
  • users, được khóa uidvà có một nametrường

Trong ứng dụng của mình, tôi hiển thị một danh sách các tin nhắn (từ messagesbộ sưu tập), với tiện ích này:

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

Nhưng bây giờ tôi muốn hiển thị tên người dùng (từ usersbộ sưu tập) cho mỗi tin nhắn.

Tôi thường gọi đó là tham gia phía khách hàng, mặc dù tôi không chắc liệu Flutter có tên cụ thể cho nó hay không.

Tôi đã tìm thấy một cách để làm điều này (mà tôi đã đăng bên dưới), nhưng tự hỏi liệu có cách nào khác / tốt hơn / thành ngữ hơn để thực hiện loại hoạt động này trong Flutter không.

Vậy: cách thành ngữ trong Flutter để tra cứu tên người dùng cho mỗi tin nhắn trong cấu trúc trên là gì?


Tôi nghĩ rằng giải pháp duy nhất tôi đã nghiên cứu rất nhiều rxdart
Cenk YAGMUR

Câu trả lời:


3

Tôi có một phiên bản khác hoạt động có vẻ tốt hơn một chút so với câu trả lời của tôi với hai nhà xây dựng lồng nhau .

Ở đây tôi cô lập về việc tải dữ liệu trong một phương thức tùy chỉnh, sử dụng một Messagelớp chuyên dụng để giữ thông tin từ một tin nhắn Documentvà người dùng liên quan tùy chọn 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;
  }
}

So với giải pháp với các trình xây dựng lồng nhau , mã này dễ đọc hơn, chủ yếu là do việc xử lý dữ liệu và trình tạo UI được phân tách tốt hơn. Nó cũng chỉ tải các tài liệu người dùng cho người dùng đã đăng tin nhắn. Thật không may, nếu người dùng đã đăng nhiều tin nhắn, nó sẽ tải tài liệu cho mỗi tin nhắn. Tôi có thể thêm bộ đệm, nhưng nghĩ rằng mã này đã hơi dài đối với những gì nó thực hiện.


1
Nếu bạn không lấy "lưu trữ thông tin người dùng bên trong tin nhắn" làm câu trả lời, tôi nghĩ đây là cách tốt nhất bạn có thể làm. Nếu bạn lưu trữ thông tin người dùng bên trong tin nhắn, có một nhược điểm rõ ràng là thông tin người dùng có thể thay đổi trong bộ sưu tập người dùng, nhưng không phải bên trong tin nhắn. Sử dụng chức năng căn cứ hỏa lực theo lịch trình, bạn cũng có thể giải quyết điều này. Thỉnh thoảng, bạn có thể truy cập bộ sưu tập tin nhắn và cập nhật thông tin người dùng theo dữ liệu mới nhất trong bộ sưu tập của người dùng.
Ugurcan Yildirim

Cá nhân tôi thích giải pháp đơn giản hơn như thế này so với kết hợp các luồng trừ khi thực sự cần thiết. Thậm chí tốt hơn, chúng ta có thể cấu trúc lại phương thức tải dữ liệu này thành một thứ như lớp dịch vụ hoặc theo mẫu BLoC. Như bạn đã đề cập, chúng tôi có thể lưu thông tin người dùng trong Map<String, UserModel>và chỉ tải tài liệu người dùng một lần.
Joshua Chan

Đồng ý Joshua. Tôi rất muốn xem một bài viết về cách nhìn này trong mẫu BLoC.
Frank van Puffelen

3

Nếu tôi đọc chính xác điều này, vấn đề trừu tượng hóa thành: làm thế nào để bạn chuyển đổi một luồng dữ liệu đòi hỏi phải thực hiện một cuộc gọi không đồng bộ để sửa đổi dữ liệu trong luồng?

Trong bối cảnh của sự cố, luồng dữ liệu là một danh sách các tin nhắn và cuộc gọi không đồng bộ là để lấy dữ liệu người dùng và cập nhật các tin nhắn có dữ liệu này trong luồng.

Có thể thực hiện điều này trực tiếp trong một đối tượng luồng Dart bằng cách sử dụng asyncMap()hàm. Đây là một số mã Dart thuần túy minh họa cách thực hiện:

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

Hầu hết các mã đang bắt chước dữ liệu đến từ Firebase dưới dạng luồng bản đồ tin nhắn và chức năng không đồng bộ để tìm nạp dữ liệu người dùng. Chức năng quan trọng ở đây là getMessagesStream().

Mã này hơi phức tạp bởi thực tế đó là một danh sách các tin nhắn đến trong luồng. Để ngăn các cuộc gọi tìm nạp dữ liệu người dùng xảy ra đồng bộ, mã sử dụng a Future.wait()để thu thập a List<Future<Message>>và tạo một List<Message>khi tất cả các Tương lai đã hoàn thành.

Trong ngữ cảnh của Flutter, bạn có thể sử dụng luồng đến từ getMessagesStream()một FutureBuilderđể hiển thị các đối tượng Message.


3

Bạn có thể làm điều đó với RxDart như thế .. 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([]);
    })
}

cho 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([]);
        })
    }

Rất tuyệt! Có cách nào để không cần f.reference.snapshots(), vì về cơ bản là tải lại ảnh chụp nhanh và tôi không muốn phụ thuộc vào khách hàng Firestore đủ thông minh để loại bỏ những thứ đó (mặc dù tôi gần như chắc chắn rằng nó đã khấu trừ).
Frank van Puffelen

Tìm thấy rồi. Thay vì Stream<Messages> messages = f.reference.snapshots()..., bạn có thể làm Stream<Messages> messages = Observable.just(f).... Điều tôi thích về câu trả lời này là nó quan sát các tài liệu người dùng, vì vậy nếu tên người dùng được cập nhật trong cơ sở dữ liệu, đầu ra phản ánh điều đó ngay lập tức.
Frank van Puffelen

Vâng làm việc rất tốt như thế tôi đang cập nhật mã của tôi
Cenk YAGMUR

1

Lý tưởng nhất là bạn muốn loại trừ bất kỳ logic kinh doanh nào, chẳng hạn như tải dữ liệu vào một dịch vụ riêng biệt hoặc theo mẫu BloC, ví dụ:

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

Sau đó, bạn có thể chỉ cần sử dụng Khối trong thành phần của mình và lắng nghe chatBloc.messagesluồng.

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

Cho phép tôi đưa ra phiên bản giải pháp RxDart của mình. Tôi sử dụng combineLatest2với một ListView.builderđể xây dựng mỗi Widget tin nhắn. Trong quá trình xây dựng mỗi tin nhắn Widget tôi tra cứu tên của người dùng tương ứng uid.

Trong đoạn trích này, tôi sử dụng tra cứu tuyến tính cho tên người dùng nhưng điều đó có thể được cải thiện bằng cách tạo uid -> user namebản đồ

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

Rất tuyệt khi thấy Arthur. Đây giống như một phiên bản sạch hơn nhiều của câu trả lời ban đầu của tôi với các nhà xây dựng lồng nhau . Chắc chắn là một trong những giải pháp đơn giản hơn để đọc.
Frank van Puffelen

0

Giải pháp đầu tiên tôi có được là lồng hai StreamBuildertrường hợp, một cho mỗi bộ sưu tập / truy vấn.

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

Như đã nêu trong câu hỏi của tôi, tôi biết giải pháp này không tuyệt vời, nhưng ít nhất nó cũng hoạt động.

Một số vấn đề tôi thấy với điều này:

  • Nó tải tất cả người dùng, thay vì chỉ những người dùng đã đăng tin nhắn. Trong các tập dữ liệu nhỏ sẽ không phải là vấn đề, nhưng khi tôi nhận được nhiều tin nhắn / người dùng hơn (và sử dụng truy vấn để hiển thị tập hợp con của chúng) tôi sẽ tải ngày càng nhiều người dùng không đăng bất kỳ tin nhắn nào.
  • Mã này không thực sự rất dễ đọc với sự lồng ghép của hai trình xây dựng. Tôi nghi ngờ đây là Flutter thành ngữ.

Nếu bạn biết một giải pháp tốt hơn, xin vui lòng gửi như một câu trả lời.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.