I am trying to add chatting to my app.
When my user starts a chat with a new user, I create a chatroom with a unique id in my Firebase database. I want my user to be notified with any updates of the chatroom document( eg: new messages) so when I create a chatroom, I also create a new stream to that chatroom document. Is there a problem with constantly listening to many documents with different streams without disposing the streams (because I want to get the latest results of any chatroom that user is a member of.)
This is my chatroom_screen code:
import 'package:flutter/material.dart';
import '../models/databse_management.dart';
class ChatroomScreen extends StatefulWidget {
static const String routeName = "/chatroom_screen";
#override
_ChatroomScreenState createState() => _ChatroomScreenState();
}
class _ChatroomScreenState extends State<ChatroomScreen> {
TextEditingController _messageTextEditingController = TextEditingController();
bool isChatroomExists;
Stream chatroomDocStream; //my initial stream variable which is null
#override
Widget build(BuildContext context) {
final Map<String, dynamic> passedArguments =
ModalRoute.of(context).settings.arguments;
//sedner details can be retrieved from currentUserDetails
final String senderId = passedArguments["senderId"];
final List<String> receiversIds = passedArguments["receiverIds"];
final String senderUsername = passedArguments["senderUsername"];
final List<String> receiverUsernames = passedArguments["receiverUsernames"];
final String chatroomId = passedArguments["chatroomId"];
if(chatroomDocStream == null){
chatroomDocStream = Firestore.instance.collection("chatrooms").document(chatroomId).snapshots(); //if no stream was created before (first time we build the widget), we connect a stream to this chatroom documentspecified with chatroomId
}
isChatroomExists = isChatroomExists == null
? passedArguments["isChatroomExists"]
: isChatroomExists;
final Image senderProfilePictureUrl =
passedArguments["senderProfilePicture"];
final List<Image> receiverProfilePictures =
passedArguments["receiverProfilePictures"];
final mediaQuery = MediaQuery.of(context).size;
final ThemeData theme = Theme.of(context);
//we get the values from the passed argument map
if (isChatroomExists) {
//load the previous chats
}
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
CircleAvatar(
backgroundImage: receiverProfilePictures[0]
.image, //right now only for 1 receiver but change it for multi members later
),
Container(
child: Text(receiverUsernames[0]),
margin: const EdgeInsets.only(left: 10),
),
],
),
),
body: //to do=> create a stream that is listening to the chatroom document for any changes
Stack(
children: <Widget>[
StreamBuilder(
//updates the chats whenever data of the chatroom document changes
stream: chatroomDocStream,
builder: (context, snapShot) {
return Column(
children: <Widget>[
Center(child: const Text("My chats"))
//message widgets go here
],
);
},
),
Positioned(
//positioned is used for positioning the widgets inside a stack. bottom: 10 means 10 pixel from bottom
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
width: mediaQuery.width, //takes the total width of the screen
decoration: BoxDecoration(
color: theme.primaryColor.withOpacity(0.3),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
//always takes the remaining space (textField widget use this as its size https://stackoverflow.com/questions/45986093/textfield-inside-of-row-causes-layout-exception-unable-to-calculate-size)
child: Container(
padding: const EdgeInsets.symmetric(
horizontal:
10), //horizontal padding for the textfield widget
decoration: BoxDecoration(
color: theme.accentColor,
borderRadius: BorderRadius.circular(25),
border: Border.all(width: 2, color: theme.primaryColor),
),
child: TextField(
minLines: 1,
maxLines: 5,
controller: _messageTextEditingController,
decoration: const InputDecoration(
hintText: "Type something here...",
border: InputBorder
.none //removes all the border for textfield widget
),
),
),
),
Container(
child: Row(
//another row for buttons to be treated all toghether as a container in the parent row
children: <Widget>[
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
//add media like image or pictures
},
),
IconButton(
icon: const Icon(Icons.camera_alt),
onPressed: () {
//take picture or video
},
),
IconButton(
icon: const Icon(Icons.send),
onPressed: () async {
if (_messageTextEditingController.text
.trim()
.isEmpty) {
return;
}
try {
await DatabaseManagement().sendMessage(
membersIds: [
senderId,
...receiversIds
], //extracts all the members of the list as seperate String items
//to do=>later on we have to get a list of the members as well
isChatroomExists: isChatroomExists,
chatroomId: chatroomId,
messageContent:
_messageTextEditingController.text,
senderId: senderId,
timestamp: Timestamp
.now(), //it's from firebase and records the time stamp of the sending message
);
if (isChatroomExists != true) {
isChatroomExists =
true; //after we sent a messsage, we 100% created the chatroom so it becomes true
}
} catch (e) {
print(
e.toString(),
);
return;
}
},
)
],
),
)
],
),
),
),
],
),
);
}
}
The idea is to have something like WhatsApp which you receive a notification of any updates in any chatroom you are a member of.
You can create a lot of snapshot listeners, but Google recommends a limit of 100 snapshot listeners per client:
Source: https://cloud.google.com/firestore/docs/best-practices?hl=en#realtime_updates
Related
When a user logs into my flutter app, they have to log in, then they are brought to a screen with a feed of posts. I use a ListView.builder to take a list of posts from my database and create the feed of posts. My issue is that when the feed screen is initially launched, the ListView doesn't load. As soon as I hot-reload the app the list does load. I imagine there's a very obvious minor mistake in my code but I just can't find it. I will put all of the code from the feed screen below, please take a look and let me know if you see the mistake.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
static const String id = "home_screen";
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// List allposts = [(post: Post, owner: String)];
Color _likeButtonColor = Colors.black;
Widget _buildPost(String username, String imageUrl, String caption) {
return Container(
color: Colors.white,
child: Column(
children: [
Container(
height: 50,
color: Colors.deepOrangeAccent[100],
child: Row(
children: [
SizedBox(width: 5),
CircleAvatar(),
SizedBox(width: 5),
Text(username, style: TextStyle(fontSize: 15)),
SizedBox(width: 225),
Icon(Icons.more_horiz)
],
),
),
Stack(
children: [
Image.asset("images/post_background.jpg"),
Padding(
padding: const EdgeInsets.all(20.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(imageUrl, fit: BoxFit.cover)),
),
],
),
Container(
height: 100,
child: Column(
children: [
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
setState(() {
HapticFeedback.lightImpact();
});
},
icon: Icon(Icons.thumb_up_alt_outlined, size: 30)),
Text("l", style: TextStyle(fontSize: 30)),
Icon(Icons.ios_share, size: 30)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(caption, style: const TextStyle(fontSize: 15))
],
)
],
),
)
],
),
);
}
List<Post> listPosts = [];
fetchPosts() async {
final userRef = FirebaseFirestore.instance.collection('users');
final QuerySnapshot result = await userRef.get();
result.docs.forEach((res) async {
print(res.id);
QuerySnapshot posts = await userRef.doc(res.id).collection("posts").get();
posts.docs.forEach((res) {
listPosts.add(Post.fromJson(res.data() as Map<String, dynamic>));
});
});
}
#override
void initState() {
fetchPosts();
print(listPosts);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: listPosts.length,
itemBuilder: (BuildContext context, int index) {
// We retrieve the post at index « index »
final post = listPosts[index];
// Replace with your actual implementation of _buildPost
return _buildPost(post.id, post.postUrlString, post.caption);
}),
);
}
}
The reason is that you need to rebuild your screen to show the reflected changes after performing an async operation (use setState to rebuild the UI). And secondly .forEach loop is not built to carry async stuff and are less efficient then a normal for loop so its better to change it.
fetchPosts() async {
final userRef = FirebaseFirestore.instance.collection('users');
final QuerySnapshot result = await userRef.get();
for(var res in result.docs)async{
print(res.id);
QuerySnapshot posts = await userRef.doc(res.id).collection("posts").get();
posts.docs.forEach((res) {
listPosts.add(Post.fromJson(res.data() as Map<String, dynamic>));
});
}
setState((){});//call it after end of your function
}
Ps:- You can use a variable named loading to show progress indicator and set it to false after fetching data in setState.
I am trying to create a list of unique "events" in my app. I have created a couple of functions to extract the data from firebase:
// event list from snapshot
List<String> _eventsFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map(
(doc) {
return doc['event'].toString() ?? '';
},
).toList();
}
//get events data
Stream<List<String>> get events {
return productCollection.snapshots().map(_eventsFromSnapshot);
}
I then want to build my list view in another screen. I have implemented my StreamProvider in the root page of my homescreen:
class OurHomePage extends StatefulWidget {
#override
_OurHomePageState createState() => _OurHomePageState();
}
class _OurHomePageState extends State<OurHomePage> {
#override
Widget build(BuildContext context) {
return StreamProvider<List<Product>>.value(
value: OurDatabase().products.handleError((e) {
print(e.toString());
}),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.center,
child: Column(
children: [
OurHeadline(),
AllCards(),
HowItWorks(),
],
),
),
),
),
);
}
}
And then I create a function to return the list of Strings and use that in my stateless widget:
class AllCards extends StatelessWidget {
#override
Widget build(BuildContext context) {
final List<String> uniqueEventList = getListOfEvents(context);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [Text('Browse all Cards'), Text('Shop All')],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
SizedBox(
height: 125,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: uniqueEventList.length,
itemBuilder: (context, i) {
return Container(
decoration: BoxDecoration(
border: Border.all(),
),
width: 160.0,
child: Center(
child: Text(uniqueEventList[i]),
),
);
},
),
)
],
),
),
);
}
List<String> getListOfEvents(BuildContext context) {
final uniqueEvents = Provider.of<List<Product>>(context);
final List<String> list = [];
for (var item in uniqueEvents) {
list.add(item.event);
}
return list.toSet().toList();
}
}
The problem is that whenever I switch pages, for a split second I get this message and an error appears:
The getter 'iterator' was called on null.
Receiver: null
Tried calling: iterator
Which indicates to me that I need to use some sort of async functionality to wait for the events data to finish loading, but is there a simple way to do this without going for something like a Future builder?
Any help would be appreciated!
I'm working on a project with Firebase (Realtime database). In this project I will have a main screen with will have several buttons according to the user. The Buttons info are going to be stored inside the realtime database. This is basically a Home Automation project.
This is how my db looks:
The quantity, means how many buttons does that user have. button1 and button2 have the button characteristics. So what I'm attempting to do is.
When the user logs in. I have a Streambuilder that will check if the quantity has data. If I has if will run inside a For loop which will create the buttons in the user screen.
I having problem getting the specific values from the database, for example, getting the quantity and storing into a variable in the main screen.
This is how I'm attempting to get the quantity (I will use this code for getting other values too, later on) but it isn't working:
Future<int> receive_quantity() async{
final FirebaseUser user = await _auth.currentUser();
var snapshot = databaseReference.child(user.uid+"/buttons"+"/quantity").once();
var result;
await snapshot.then((value) => result = value);
print(result);
return result;
}
Error that I get:
_TypeError (type 'DataSnapshot' is not a subtype of type 'FutureOr<int>')
My StreamBuilder:
body: StreamBuilder(
stream: _auth.getButtonQuantity(),
initialData: 0,
builder: (context, snapshot) {
if (snapshot.hasError || snapshot.hasError){
return Container(color: Colors.red);
}
if (!snapshot.hasData || !snapshot.hasData){
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasData || snapshot.hasData){
return GridView.count(
padding: EdgeInsets.all(15),
crossAxisSpacing: 20.0,
mainAxisSpacing: 20.0,
crossAxisCount: 3,
children: [
for (int i = 0; i < buttonquant; i++){
Button(),
},
GestureDetector(
onTap: () async{
_auth.receive_quantity();
},
child: Container(
color: Colors.black,
width: 150,
height: 150,
child: Icon(Icons.add, color: Colors.white,),
),
),
],
);
}
}
),
My Button:
class Button extends StatelessWidget {
const Button({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
},
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(15)
),
child: Stack(
children: [
Positioned(
top: 10,
left: 10,
child: Icon(
Icons.lightbulb,
size: 35,
color: Colors.white,
),
),
Positioned(
top: 95,
left: 15,
child: Text("Televisao", style: TextStyle(color: Colors.white),),
),
],
),
),
);
}
}```
What you need to do is you need to get the value of the snapshot not using it directly:
Future<int> receive_quantity() async{
final FirebaseUser user = await _auth.currentUser();
var snapshot = await databaseReference.child(user.uid+"/buttons"+"/quantity").once();
var result = snapshot.value; //get the value here
print(result);
return result;
}
This is how you get the value in general:
databaseReference.once().then((DataSnapshot snapshot) {
print('Data : ${snapshot.value}');
});
I am making a chat app that displays both a Group Chat and Private Chat in the same List.
I use Firestore as the database and store the data of User, Group and Contact in there. I have a Message Screen that displays a list of Chats that the User has using StreamBuilder.
I want to display data differently depending on the group's data. The group chat has their Group picture, Private Chat with User in Contact, their avatar display, and Private Chat with a generic icon display with User not in Contact.
I iterate through the stream first in a DatabaseService class, then put it in a variable and set it as a stream for StreamBuilder. This works fine, but I also want a list to check if a user already has a private chat with another User without getting the data from Firestore.
API.dart
//this is where I put my code to connect and read/write data from Firestore
final FirebaseFirestore _db = FirebaseFirestore.instance;
Api();
....
Stream<QuerySnapshot> streamCollectionByArrayAny(
String path, String field, dynamic condition) {
return _db
.collection(path)
.where(field, arrayContainsAny: condition)
.snapshots();
}
DatabaseService.dart
...
List<GroupModel> groups; //List of Groups
Stream<List<GroupModel>> groupStream; //Stream of List Group
...
Stream<QuerySnapshot> fetchGroupsByMemberArrayAsStream(
String field, dynamic condition) {
return _api.streamCollectionByArrayAny('groups', field, condition);
}
//function to get Contact Detail using List of Group User
Future<ContactModel> getContactDetail(List<dynamic> members) async {
//remove current user id from the list
members.removeWhere((element) => element.userId == user.userId);
//getContactbyId return a ContactModel object from Firestore
ContactModel contactModel =
await getContactById(user.userId, members.first.userId);
if (contactModel != null && contactModel.userId.isNotEmpty) {
return contactModel;
} else {
return new ContactModel(
userId: members.first.userId, nickname: "", photoUrl: "");
}
}
Future<GroupModel> generateGroupMessage(GroupModel group) async {
//check if Group Chat or Private chat
if (group.type == 1) {
ContactModel contactModel = await getContactDetail(group.membersList);
group.groupName = contactModel.nickname.isNotEmpty
? contactModel.nickname
: contactModel.userId;
group.groupPhoto = contactModel.photoUrl;
}
print("Add");
//add the group data into List<GroupModel> groups
groups.add(group);
return group;
}
void refreshMessageList() {
groups = [];
print("refresh");
//get Group Data as Stream from FireStore base on the user data in the Member Array of Group then map it to Stream while also change data base on Group type in generateGroupMessage
groupStream = fetchGroupsByMemberArrayAsStream('membersList', [
{"isActive": true, "role": 1, "userId": user.userId},
{"isActive": true, "role": 2, "userId": user.userId}
]).asyncMap((docs) => Future.wait([
for (GroupModel group in docs.docs
.map((doc) => GroupModel.fromMap(doc.data()))
.toList())
generateGroupMessage(group)
]));
}
Message.dart
#override
void initState() {
super.initState();
...
databaseService.refreshMessageList();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.symmetric(horizontal: 16),
margin: EdgeInsets.only(top: 24),
child: Column(
children: [
...
Flexible(
child: StreamBuilder(
stream: databaseService.groupStream,
builder: (context, AsyncSnapshot<List<GroupModel>> snapshot) {
if (!snapshot.hasData) {
print("No data");
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
),
);
} else {
print("Has data");
groups = List.from(snapshot.data);
groups.removeWhere(
(element) => element.recentMessageContent.isEmpty);
groups.sort((group1, group2) {
if (DateTime.parse(group1.recentMessageTime)
.isAfter(DateTime.parse(group2.recentMessageTime))) {
return -1;
} else {
return 1;
}
});
return ListView.builder(
padding: EdgeInsets.all(10.0),
itemBuilder: (context, index) =>
buildItem(context, groups[index]),
itemCount: groups.length,
),
),
),
}
],)));
}
Widget buildItem(BuildContext context, GroupModel group) {
if (group.recentMessageContent == '') {
return Container();
} else {
return Column(
children: [
Container(
child: InkWell(
child: Row(
children: <Widget>[
Material(
child: group.groupPhoto.isNotEmpty
? CachedNetworkImage(
placeholder: (context, url) => Container(
child: CircularProgressIndicator(
strokeWidth: 1.0,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.grey),
),
width: 60.0,
height: 60.0,
padding: EdgeInsets.all(10.0),
),
imageUrl: group.groupPhoto,
width: 60.0,
height: 60.0,
fit: BoxFit.cover,
)
: Icon(
group.type == 1
? Icons.account_circle
: Icons.group,
size: 60.0,
color: Colors.grey,
),
borderRadius: BorderRadius.all(Radius.circular(30.0)),
clipBehavior: Clip.hardEdge,
),
SizedBox(
width: 150,
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
group.groupName,
style: TextStyle(
color: colorBlack,
fontSize: 12,
fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
Text(
group.recentMessageContent,
style: TextStyle(
color: Colors.grey,
fontSize: 10,
height: 1.6),
overflow: TextOverflow.ellipsis,
),
],
),
margin: EdgeInsets.only(left: 12.0),
),
),
Spacer(),
Text(
formatDateTime(group.recentMessageTime),
style: TextStyle(color: Colors.grey, fontSize: 10),
),
],
),
onTap: () {
switch (group.type) {
case 1:
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(
settings:
RouteSettings(name: "/message/chatPage"),
builder: (context) => ChatPage(group: group)))
.then((value) => setState);
break;
case 2:
Navigator.of(context, rootNavigator: true)
.push(MaterialPageRoute(
settings:
RouteSettings(name: "/message/chatGroup"),
builder: (context) =>
ChatGroupPage(group: group)))
.then((value) => {setState(() {})});
break;
}
}),
),
Divider(
color: Colors.grey,
),
],
);
}
}
The ChatPage and ChatGroupPage navigate to Private Chat and Group Chat respectively, and in there the User can add the chat partner or group member into Contact.
When adding is done I call the databaseService.refreshMessageList to refresh the Stream of List Group, so when I navigate back to the Message Screen, it will refresh and display accordingly. However, the List<GroupModel> groups becomes blank and will not add data until I navigate back to the Message Screen.
I debugged the app and found that the List became blank because it executes groups = [] but did not run the .asyncMap until I hot reload or navigate Message Screen and put the setState in .then to refresh the data.
I need the List groups to check whether the 2 users already have a private chat to create a new one when adding to Contact. I have already tried putting setState after databaseService.refreshMessageList, but it still did not work.
Can anyone please help me and provide a solution? I know this is not a good question to ask, but I have been stuck with this for almost a week now and desperately need an answer. Thank you in advance.
EDIT
Here is my data structure:
Users
/users (collection)
/userId
/user (document)
- userId
- nickname
- photoUrl
- token
- /contacts (subcollection)
/contactId
/contact (document)
- userId
- nickname
- photoUrl
Groups:
/groups (collection)
/groupId
/group (document)
- groupId
- groupName
- type
- membersList (List<Map<String, dynamic>>)
- member: userId, isActive, role
- recentMessageContent
- recentMessageTime
- recentMessageType
Messages:
/messages (collection)
/groupId
/groupMessage (document)
/messages (subcollection)
/messageId
/message (document)
- messageContent
- messageTime
- messageType
You can use array membership, for example, the array-contains method can query for elements within an array without performing any manipulation. There is an interesting article that provides some examples you might interest you.
Another alternative could be to iterate both arrays until matching the values you need. However, iteration can lead to performance issues if you do not implement it correctly.
I want to build a view to show some events inside a listview in my app like this:
I have these two tables:
Users
Events
But I don't know how do a "inner join" between the tables USERS and EVENTS...
I tried this:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:project/Methods.dart';
import 'package:project/Views/CadastroUsuario.dart';
import 'dart:math';
class EventClass{
String owner;
String description;
String city;
String state;
String place;
}
class EventsListing extends StatefulWidget {
#override
EventsListingState createState() => new EventsListingState();
}
class EventsListingState extends State<EventsListing> {
List<EventClass> events;
#override
void initState() {
super.initState();
events = new List<EventClass>();
}
void buildEventClass(DocumentSnapshot doc) async {
EventClass oneEvent = new EventClass();
DocumentReference document = Firestore.instance.collection("users").document(doc["userid"]);
document.get().then((DocumentSnapshot snapshot){
oneEvent.owner = snapshot["name"].toString();
});
oneEvent.description = doc["description"];
oneEvent.place = doc["place"];
oneEvent.city = doc["city"];
oneEvent.state = doc["state"];
events.add(oneEvent);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Events'),
),
body: new StreamBuilder(
stream: Firestore.instance.collection("events").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if (snapshot.connectionState == ConnectionState.waiting)
return Text("Loading...");
return new ListView(
padding: EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0),
children: snapshot.data.documents.map((document){
buildEventClass(document);
return events.length == 0 ? new Card() : item(events.last);
}).toList()
);
},
),
floatingActionButton: new FloatingActionButton(
tooltip: 'New',
child: new Icon(Icons.add),
onPressed: () async {
Navigation navigation = new Navigation();
navigation.navigaTo(context, CadastroUsuario());
},
),
);
}
Widget item(EventClass oneEvent) {
return new Card(
elevation: 4.0,
child: new Column(
children: <Widget>[
new Row(
children: <Widget>[
new Column(
children: <Widget>[
new Text(oneEvent.owner.toString(),
style: TextStyle(fontSize: 20.0),
overflow: TextOverflow.ellipsis,),
],
),
new Column(
children: <Widget>[
],
)
],
),
new Container(
color: Colors.blue,
height: 150.0,
),
new Row(
children: <Widget>[
new Row(
children: <Widget>[
new Text(oneEvent.description.toString(),
style: TextStyle(fontSize: 20.0),
overflow: TextOverflow.ellipsis,),
],
),
new Row(
children: <Widget>[
new Text(oneEvent.place.toString(),
style: TextStyle(color: Colors.grey[350]),
overflow: TextOverflow.ellipsis,),
],
),
new Row(
children: <Widget>[
new Text(oneEvent.city.toString() +' - '+ oneEvent.state.toString(),
style: TextStyle(color: Colors.grey[350]),
overflow: TextOverflow.ellipsis,),
],
)
]
)
],
)
);
}
}
But every time that I try to show these events I get this exception
Exception has occurred.
PlatformException(error, Invalid document reference. Document references must have an even number of segments, but users has 1, null)
What I'm doing wrong? How I can do a "inner join" between thesse tables and show the events?
I'm using the Firebase Firestore.
PS: I already know that Firestore is a noSQL database and have no "joins", but I want to do something like a join.
As I was telling in the coments Firestore does not support multi collection querys cause its no relational DB. If you need to access multiple collections you would manage querys independently.
This is how I usually get related collections data (Sorry this is JS code but I dont know DART):
var data = {};
//First you get users data
DocumentReference document = Firestore.collection("users")
document.get().then((snapshot) => {
//In this case I will store data in some object, so I can add events as an array for a key in each user object
snapshot.forEach((userDoc) => {
var userDocData = userDoc.data()
if (data[userDoc.id] == undefined) {
data[userDoc.id] = userDocData
}
})
//So in this moment data object contains users, now fill users with events data
//In this var you count how many async events have been downloaded, with results or not.
var countEvents = 0
Object.keys(data).forEach((userDocId) => {
//Here Im creating another query to get all events for each user
SnapshotReference eventsForCurrentUserRef = Firestore.collection("events").where("userId", "==", userDocId)
eventsForCurrentUserRef.get.then((eventsForUserSnapshot) => {
//Count events
countEvents++
eventsForUserSnapshot.forEach((eventDoc) => {
var eventDocData = eventDoc.data()
//Check if array exists, if not create it
if (data[eventDocData.userId].events == undefined) {
data[eventDocData.userId].events = []
}
data[eventDocData.userId].events.push(eventDocData)
})
if(countEvents == Object.keys(data).length){
//Lookup for events in every user has finished
}
})
})
})