Flutter'daki iki Firestore koleksiyonundan verileri nasıl birleştiririm?


9

Firestore'u kullanarak Flutter'da bir sohbet uygulamam var ve iki ana koleksiyonum var:

  • chatsHangi otomatik kimlikleri üzerine kademeli ve vardır message, timestampve uidalanları.
  • users, anahtarlıdır uidve bir namealanı vardır

Uygulamamda messages, bu widget ile mesajların bir listesini ( koleksiyondan) gösteririm:

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

Ama şimdi usersher mesaj için kullanıcının adını ( koleksiyondan) göstermek istiyorum .

Flutter'ın bunun için belirli bir adı olup olmadığından emin olmasam da normalde istemci tarafı birleştirme olarak adlandırıyorum.

Bunu yapmanın bir yolunu buldum (aşağıda yayınladım), ancak Flutter'da bu tür bir işlem yapmanın başka / daha iyi / daha deyimsel bir yolu olup olmadığını merak ediyorum.

Peki: Flutter'da yukarıdaki yapıdaki her mesajın kullanıcı adını aramanın deyimsel yolu nedir?


Bence bir sürü rxdart araştırdım tek çözüm
Cenk YAGMUR

Yanıtlar:


3

İki iç içe inşaatçı ile cevabımdan biraz daha iyi görünen başka bir versiyonum var .

Burada Messagebir mesaj Documentve isteğe bağlı ilişkili kullanıcı bilgilerini tutmak için özel bir sınıf kullanarak, özel bir yöntemde veri yükleme izole 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;
  }
}

İç içe geçmiş oluşturucularla çözümle karşılaştırıldığında, bu kod daha çok okunabilir, çünkü çoğunlukla veri işleme ve kullanıcı arabirimi oluşturucu daha iyi ayrılır. Ayrıca kullanıcı belgelerini yalnızca mesaj gönderen kullanıcılar için yükler. Ne yazık ki, kullanıcı birden çok mesaj yayınladıysa, her mesaj için dokümanı yükleyecektir. Bir önbellek ekleyebilirim, ancak bu kodun başardıkları için zaten biraz uzun olduğunu düşünüyorum.


1
Cevap olarak "kullanıcı bilgilerinin mesajın içinde saklanması" nı almazsanız, bunun yapabileceğiniz en iyi şey olduğunu düşünüyorum. Kullanıcı bilgilerini mesajın içinde saklarsanız, kullanıcı bilgilerinin kullanıcı koleksiyonunda değişebileceği ancak mesajın içinde değişmeyeceği bariz bir dezavantajı vardır. Zamanlanmış bir firebase işlevi kullanarak bunu da çözebilirsiniz. Zaman zaman mesajlar koleksiyonuna gidebilir ve kullanıcı bilgilerini kullanıcı koleksiyonundaki en son verilere göre güncelleyebilirsiniz.
Uğurcan Yıldırım

Şahsen, gerçekten gerekli olmadıkça akışları birleştirmekten daha basit bir çözümü tercih ederim. Daha da iyisi, bu veri yükleme yöntemini bir hizmet sınıfı gibi bir şeye yeniden düzenleyebilir veya BLoC modelini takip edebiliriz. Daha önce de belirttiğiniz gibi, kullanıcı bilgilerini bir kaydedebilir Map<String, UserModel>ve kullanıcı belgesini yalnızca bir kez yükleyebiliriz.
Joshua Chan

Joshua kabul etti. Bunun BLoC modelinde nasıl göründüğüne dair bir yazı görmek isterim.
Frank van Puffelen

3

Bunu doğru okuyorsam, sorun şu şekilde özetlenir: akıştaki verileri değiştirmek için eşzamansız bir çağrı yapılmasını gerektiren bir veri akışını nasıl dönüştürürsünüz?

Sorun bağlamında, veri akışı bir mesaj listesidir ve zaman uyumsuz çağrı, kullanıcı verilerini almak ve mesajları akıştaki bu verilerle güncellemektir.

Bunu asyncMap()işlevi kullanarak doğrudan bir Dart akışı nesnesinde yapmak mümkündür . İşte nasıl yapılacağını gösteren bazı Dart kodları:

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

Kodun çoğu, Firebase'den gelen iletilerin bir haritası ve kullanıcı verilerini getirmek için zaman uyumsuz bir işlev olarak gelen verileri taklit ediyor. Buradaki önemli fonksiyon getMessagesStream().

Kod, akışa gelen iletilerin bir listesi olması nedeniyle biraz karmaşıktır. Kullanıcı verilerinin getirilmesine yönelik çağrıların eşzamanlı olarak gerçekleşmesini önlemek için kod, a Future.wait()a List<Future<Message>>ve List<Message>tüm Futures'lar tamamlandığında bir a oluşturmak için a kullanır.

Flutter bağlamında , Mesaj nesnelerini görüntülemek için getMessagesStream()a öğesinden gelen akışı kullanabilirsiniz FutureBuilder.


3

Bunu RxDart ile böyle yapabilirsiniz .. 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 için

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

Çok havalı! f.reference.snapshots()Aslında bu anlık görüntüyü yeniden yükleme gerek yok bir yolu var mı ve ben (neredeyse tekil olmasına rağmen neredeyse eminim) Firestore istemci bunları tekilleştirmek için yeterince akıllı olmak güvenmemeyi tercih ederim.
Frank van Puffelen

Buldum. Bunun yerine Stream<Messages> messages = f.reference.snapshots()..., yapabilirsiniz Stream<Messages> messages = Observable.just(f).... Bu cevap hakkında ne gibi kullanıcı belgeleri gözlemler, yani veritabanında bir kullanıcı adı güncelleştirilirse, çıktı hemen yansıtır.
Frank van Puffelen

Evet kodumu güncelleyerek im çok iyi çalışıyor
Cenk YAGMUR

1

İdeal olarak, veri yükleme gibi herhangi bir iş mantığını ayrı bir hizmete veya BloC modelini izleyerek hariç tutmak istersiniz, örneğin:

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

Ardından, bileşeninizdeki Bloku kullanabilir ve chatBloc.messagesakışı dinleyebilirsiniz .

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

Bir RxDart çözümü versiyonumu ortaya koymama izin verin. Ben her mesaj Widget oluşturmak için bir combineLatest2ile kullanın ListView.builder. Her mesajın Widget yapımı sırasında karşılık gelen kullanıcının adını ararım uid.

Bu snippet'te kullanıcının adı için doğrusal bir arama kullanıyorum, ancak bu bir uid -> user nameharita oluşturarak geliştirilebilir

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

Arthur'u görmek çok güzel. Bu, iç içe geçmiş inşaatçılar ile ilk cevabımın daha temiz bir versiyonu gibi . Kesinlikle daha basit çözümlerden biri.
Frank van Puffelen

0

Çalıştığım ilk çözüm StreamBuilder, her koleksiyon / sorgu için bir tane olmak üzere iki örneği yuvalamaktır .

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

Sorumda belirtildiği gibi, bu çözümün harika olmadığını biliyorum, ama en azından işe yarıyor.

Bununla ilgili gördüğüm bazı sorunlar:

  • Yalnızca ileti gönderen kullanıcılar yerine tüm kullanıcıları yükler. Sorun olmayan küçük veri kümelerinde, ancak daha fazla ileti / kullanıcı aldığımda (ve alt kümesini göstermek için bir sorgu kullandığımda), ileti göndermeyen daha fazla kullanıcı yükleyeceğim.
  • Kod, iki üreticinin yuvalanmasıyla gerçekten okunabilir değil. Bunun deyimsel Flutter olduğundan şüpheliyim.

Daha iyi bir çözüm biliyorsanız, lütfen yanıt olarak gönderin.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.