ฉันจะเข้าร่วมข้อมูลจากสองคอลเล็กชัน Firestore ใน Flutter ได้อย่างไร


9

ฉันมีแอพแชทใน Flutter โดยใช้ Firestore และฉันมีคอลเลกชันหลักสอง:

  • chatsซึ่งเป็นคีย์อัตโนมัติรหัสและมี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
Cenk YAGMUR

คำตอบ:


3

ฉันได้เวอร์ชั่นอื่นที่ใช้งานได้ซึ่งดีกว่าคำตอบของฉันกับผู้สร้างสองซ้อนกันเล็กน้อย

นี่ฉันโดดเดี่ยวในการโหลดข้อมูลในวิธีการที่กำหนดเองโดยใช้เฉพาะMessageชั้นเพื่อเก็บข้อมูลจากข้อความและการใช้งานที่เกี่ยวข้องไม่จำเป็นDocumentDocument

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

เปรียบเทียบกับวิธีแก้ปัญหากับผู้สร้างที่ซ้อนกันรหัสนี้สามารถอ่านได้มากขึ้นส่วนใหญ่เป็นเพราะการจัดการข้อมูลและตัวสร้าง UI จะแยกกันดีกว่า นอกจากนี้ยังโหลดเอกสารผู้ใช้สำหรับผู้ใช้ที่โพสต์ข้อความเท่านั้น น่าเสียดายที่หากผู้ใช้โพสต์ข้อความหลายข้อความมันจะโหลดเอกสารสำหรับแต่ละข้อความ ฉันสามารถเพิ่มแคชได้ แต่คิดว่ารหัสนี้มีความยาวพอสมควรสำหรับสิ่งที่มันทำสำเร็จ


1
หากคุณไม่ใช้ "การจัดเก็บข้อมูลผู้ใช้ภายในข้อความ" เป็นคำตอบฉันคิดว่านี่เป็นวิธีที่ดีที่สุดที่คุณสามารถทำได้ หากคุณเก็บข้อมูลผู้ใช้ไว้ในข้อความมีข้อเสียเปรียบที่ชัดเจนว่าข้อมูลผู้ใช้อาจเปลี่ยนแปลงในการรวบรวมผู้ใช้ แต่ไม่ใช่ภายในข้อความ การใช้ฟังก์ชั่น firebase ตามกำหนดการคุณอาจแก้ไขปัญหานี้ได้ ในบางครั้งคุณสามารถผ่านการรวบรวมข้อความและอัปเดตข้อมูลผู้ใช้ตามข้อมูลล่าสุดในการรวบรวมผู้ใช้
Ugurcan Yildirim

โดยส่วนตัวแล้วฉันชอบโซลูชันที่ง่ายกว่าเช่นนี้เมื่อเทียบกับการรวมสตรีมหากไม่จำเป็นจริงๆ ยิ่งไปกว่านั้นเราสามารถปรับเปลี่ยนวิธีการโหลดข้อมูลนี้เป็นสิ่งที่เหมือนคลาสบริการหรือตามรูปแบบ BLoC ดังที่คุณได้กล่าวไปแล้วเราสามารถบันทึกข้อมูลผู้ใช้ในMap<String, UserModel>และโหลดเอกสารผู้ใช้เพียงครั้งเดียว
Joshua Chan

เห็นด้วยกับโจชัว ฉันชอบที่จะเห็นบทความนี้ว่าเป็นอย่างไรในรูปแบบ BLoC
Frank van Puffelen

3

หากฉันอ่านข้อความนี้อย่างถูกต้องปัญหาจะเปลี่ยนไปเป็น: คุณจะแปลงกระแสข้อมูลที่ต้องใช้การโทรแบบอะซิงโครนัสเพื่อปรับเปลี่ยนข้อมูลในสตรีมได้อย่างไร

ในบริบทของปัญหาสตรีมของข้อมูลคือรายการข้อความและการเรียก async คือการดึงข้อมูลผู้ใช้และอัปเดตข้อความด้วยข้อมูลนี้ในสตรีม

เป็นไปได้ที่จะทำสิ่งนี้โดยตรงในวัตถุ Dart stream โดยใช้asyncMap()ฟังก์ชั่น นี่คือบางส่วนของรหัส 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 เป็นกระแสข้อมูลแผนที่และฟังก์ชั่น async เพื่อดึงข้อมูลผู้ใช้ getMessagesStream()ฟังก์ชั่นสำคัญที่นี่คือ

รหัสมีความซับซ้อนเล็กน้อยเนื่องจากข้อเท็จจริงว่าเป็นรายการข้อความที่เข้ามาในสตรีม เพื่อป้องกันการเรียกข้อมูลผู้ใช้จากการซิงโครไนซ์รหัสใช้Future.wait()เพื่อรวบรวมList<Future<Message>>และสร้างList<Message>เมื่อฟิวเจอร์สทั้งหมดเสร็จสมบูรณ์

ในบริบทของ Flutter คุณสามารถใช้กระแสที่มาจาก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()เพราะมันเป็นสิ่งที่โหลดใหม่สแน็ปช็อตและฉันไม่ต้องการพึ่งพาลูกค้าของ Firestore ที่ฉลาดพอที่จะทำสำเนาซ้ำซ้อน (แม้ว่าฉันเกือบจะแน่ใจว่ามันซ้ำซ้อน)
Frank van Puffelen

พบมัน แทนการที่คุณสามารถทำได้Stream<Messages> messages = f.reference.snapshots()... Stream<Messages> messages = Observable.just(f)...สิ่งที่ฉันชอบเกี่ยวกับคำตอบนี้คือมันตรวจสอบเอกสารผู้ใช้ดังนั้นหากชื่อผู้ใช้ถูกอัพเดตในฐานข้อมูลเอาต์พุตจะแสดงผลทันที
Frank van Puffelen

ใช่ทำงานได้ดีเช่นนั้นฉันอัปเดตรหัสของฉัน
Cenk YAGMUR

1

เป็นการดีที่คุณต้องการยกเว้นตรรกะทางธุรกิจใด ๆ เช่นการโหลดข้อมูลลงในบริการที่แยกต่างหากหรือทำตามรูปแบบ 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());
          }
        });
  }
}

1

อนุญาตให้ฉันใช้ RxDart ทางออกรุ่นของฉัน ฉันใช้combineLatest2กับ a ListView.builderเพื่อสร้างวิดเจ็ตข้อความแต่ละรายการ ในระหว่างการก่อสร้างของแต่ละข้อความที่ Widget 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';
  }
}

เด็ดมากที่เห็นอาเธอร์ นี้เป็นเหมือนรุ่นทำความสะอาดมากครั้งแรกของฉันคำตอบกับผู้ที่ซ้อนกัน หนึ่งในวิธีแก้ปัญหาที่ง่ายกว่าในการอ่าน
Frank van Puffelen

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

ตามที่ระบุไว้ในคำถามของฉันฉันรู้ว่าวิธีนี้ไม่ดี แต่อย่างน้อยก็ใช้งานได้

ปัญหาบางอย่างที่ฉันเห็นในสิ่งนี้:

  • มันโหลดผู้ใช้ทั้งหมดแทนที่จะเป็นเพียงผู้ใช้ที่โพสต์ข้อความ ในชุดข้อมูลขนาดเล็กที่ไม่มีปัญหา แต่เมื่อฉันได้รับข้อความ / ผู้ใช้มากขึ้น (และใช้แบบสอบถามเพื่อแสดงชุดย่อยของพวกเขา) ฉันจะโหลดผู้ใช้มากขึ้นเรื่อย ๆ ที่ไม่โพสต์ข้อความใด ๆ
  • รหัสไม่สามารถอ่านได้อย่างแท้จริงกับการซ้อนกันของสองผู้สร้าง ฉันสงสัยว่านี่เป็นสำนวนโวหาร

หากคุณรู้วิธีแก้ปัญหาที่ดีกว่าโปรดโพสต์เป็นคำตอบ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.