Flutter에서 두 Firestore 컬렉션의 데이터를 결합하려면 어떻게합니까?


9

Firestore를 사용하여 Flutter에 채팅 앱이 있고 두 가지 주요 컬렉션이 있습니다.

  • 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을 많이 연구 한 유일한 솔루션 생각
셍크 YAGMUR

답변:


3

두 개의 중첩 된 빌더로 내 대답 보다 약간 더 나은 다른 버전이 작동 합니다.

여기에서는 전용 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;
  }
}

중첩 된 빌더 가있는 솔루션과 비교할 때이 코드는 데이터 처리와 UI 빌더가 더 잘 분리되어 있기 때문에 더 읽기 쉽습니다. 또한 메시지를 게시 한 사용자에 대한 사용자 문서 만로드합니다. 불행히도 사용자가 여러 메시지를 게시 한 경우 각 메시지에 대한 문서를로드합니다. 캐시를 추가 할 수는 있지만이 코드는 이미 수행하는 데 약간 길다고 생각합니다.


1
"메시지에 사용자 정보 저장"을 대답으로 사용하지 않으면 이것이 최선의 방법이라고 생각합니다. 메시지에 사용자 정보를 저장하면 사용자 정보가 사용자 컬렉션에서 변경 될 수 있지만 메시지 내부에서는 변경되지 않을 수 있다는 명백한 단점이 있습니다. 예약 된 Firebase 기능을 사용하면이 문제를 해결할 수도 있습니다. 때때로 메시지 수집을 통해 이동하고 사용자 수집의 최신 데이터에 따라 사용자 정보를 업데이트 할 수 있습니다.
우 구르 칸 일 디림

개인적으로 필자는 실제로 필요한 경우가 아니라면 스트림 결합과 비교하여 이와 같은 간단한 솔루션을 선호합니다. 또한이 데이터 로딩 방법을 서비스 클래스와 같은 것으로 리팩터링하거나 BLoC 패턴을 따를 수 있습니다. 이미 언급했듯이 사용자 정보를에 저장 Map<String, UserModel>하고 사용자 문서를 한 번만로드 할 수 있습니다.
Joshua Chan

동의 여호수아. 나는 이것이 BLoC 패턴으로 어떻게 보이는지에 대한 글을 쓰고 싶다.
Frank van Puffelen

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()입니다.

스트림에서 오는 메시지 목록이라는 사실 때문에 코드가 약간 복잡합니다. 사용자 데이터를 가져 오기위한 호출이 동 기적으로 발생하는 것을 방지하기 위해 코드는 a Future.wait()를 사용하여 a 를 수집하고 모든 선물이 완료되었을 때를 List<Future<Message>>만듭니다 List<Message>.

Flutter와 관련 getMessagesStream()하여 a 에서 나오는 스트림을 사용 FutureBuilder하여 Message 객체를 표시 할 수 있습니다 .


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

그런 다음 컴포넌트에서 블록을 사용하고 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 솔루션을 발표 할 수 있습니다. 내가 사용 combineLatest2ListView.builder각 메시지 위젯을 구축 할 수 있습니다. 각 메시지 위젯을 구성하는 동안 해당 이름으로 사용자 이름을 조회합니다 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';
  }
}

Arthur를 만나서 매우 시원합니다. 이것은 중첩 된 빌더를 사용한 초기 답변 의 훨씬 깨끗한 버전과 같습니다 . 보다 간단한 읽기 쉬운 솔루션 중 하나입니다.
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.