So I made some childs in Firebase and wanted to load them as a list view in Flutter, so I made a Scaffold and this is whats inside the Body:
FutureBuilder(
future: _Ref.get(),
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator.adaptive();
}
if(snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
List<Widget> _list = [];
_Ref.once().then((DataSnapshot) async {
final data = await DataSnapshot.snapshot.children.toList();
data.forEach((element) async {
final value = await element.value.toString();
print(value);
_list.add(ListTile(
title: Text(value),
trailing: Icon(
Icons.delete_outlined,
),
));
});
});
print(_list);
return ListView(
children: _list,
);
}
},
),
my output looks like this:
this is what my Firebase database looks like:
But the funny thing is that if I hotreload my app it works how it is supposed to
My educated guess is that you expected 2 lines in the output, instead of the 3 lines you get.
When the Firebase SDKs see sequential, numeric keys in a snapshot, they assume that this snapshot is an array. If there's one or a few keys missing in that array, it fill them with null values. So in your database screenshot, the Firebase SDK sees an array with indexes 1 and 2, and index 0 missing.
If you want to prevent this array coercion, don't use sequential numeric keys in your data structure. You can most easily prevent this by calling push() to generate a unique key for you, or you can prefix the existing values with a short non-numeric string, e.g. "key1", "key2".
I recommend checking out this vintage blog post: Best Practices: Arrays in Firebase on why using such array-like keys is typically an antipattern in Firebase.
Related
As the title suggests, I have a stream that delivers through a StreamBuilder an AsyncSnapshot<List>, a list of elements. The elements then are built in a ReorderableListView.builder in order to rearrange them through drag & drop.
Widget - View
body: Column(children: [
StreamBuilder(
initialData: controller.listOfProjectTasks,
stream: controller.retrieveTasksOfProject(),
builder: (BuildContext context,
AsyncSnapshot<List<Task>> dataSnapshot) {
return Expanded(
child: GetBuilder<BacklogController>(
init: Get.find<BacklogController>(),
builder: (value) {
if (dataSnapshot.hasData) {
return ReorderableListView.builder(
onReorder: (int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final Task? taskToOrder =
dataSnapshot.data?.removeAt(oldIndex);
dataSnapshot.data?.insert(newIndex, taskToOrder!);
controller.saveNewTaskPositions(dataSnapshot.data);
},
itemCount: dataSnapshot.data!.length,
itemBuilder: (context, int index) {
GlobalKey globalKey = GlobalKey();
return Draggable(
feedback: Container(
width: 10,
),
child: TaskTile(
dataSnapshot.data![index]),
key: globalKey,
);
});
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
}));
})
]),
View model Stream generator
yield* Stream.periodic(const Duration(seconds: 5), (_) {
return backlogRepository.retrieveTasks(projectId);
}).asyncMap((value) async => await value);
Problem I: I need to save the Task ordering and I face 2 challenges, on one side whenever the Builder delivers the list and I reorder it, the stream yields the same data again with the previous order, and the ListView gets built with the previous order.
Problem II: the list ordering would need to be preserved on App restart
Possible hack: I'm using the Flutter Firestore combo so I have a document-oriented DB, no Autoincrement on IDs like in relational db's. The solution that I came up with is trivial and essentially I add a new field to the Task element named "order".
Sample Task Document Model, should be a toJson method in Model class
Future<void> addNewTask(String projectId, String createdProjectName,
String startDate, String endDate) async {
firestore.collection('task').add({
'name': createdProjectName,
'startDate': startDate,
'endDate': endDate,
'teamId': '',
'projectId': projectId,
'epicId': '',
'order': 1,
'teamMemberId': '',
'position': null
});
}
Now problem is that the List is preserving a Scrum Backlog, for scalability issues I'm assuming that it could hold 1000 Tasks related to 1 Project.
With 1 rearrangement of Tasks I could on worst case assume 999 Firestore calls to just update each Task document field "order", and it's only on 1 Project on Firestore.
Is there a better way to act on this problem?
Possible changes that I see are on:
How Task documents are stored -> how to address the order persistence
How the field "order" of each Task document can be updated through the Firestore call on the Repository
Any suggestions?
What I want
To order ListView based on server timestamp
My code
Adding to firestore collection:
onPressed: () {
FirebaseFirestore.instance.collection('things').add(
{
// some code
'timestamp': FieldValue.serverTimestamp(),
},
);
Get data from the firestore and ordered them based on the server timestamp
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('things').orderBy('timestamp').snapshots(),
Expected behavior
List View are displayed ordered based on the server timestamp
What I got
This error:
Expected a value of type 'String', but got one of type 'Timestamp'
What I've tried
I've tried adding toString() when sending the data to the firestore: 'timestamp': FieldValue.serverTimestamp().toString(), but then the data on the firestore didn't store timestamp, instead they stored FieldValue(Instance of 'FieldValueWeb').
I know that I probable have to convert them to String when I'm getting the data from the firestore, but I have no idea how to do that. I've tried adding in toString() when getting the data into the stream as such:
stream: FirebaseFirestore.instance.collection('things').orderBy('timestamp').toString().snapshots()
but then it shows below error and won't compile.
The method 'snapshots' isn't defined for the type 'String'.
The official document does not say anything about converting them to String either.
If anybody knows how to solve this please help me, I'm really stuck here.
Full StreamBuilder and ListView code
return StreamBuilder<QuerySnapshot>(
stream: _firestore.collection('things').orderBy('timestamp').snapshots(),
builder: (context, snapshot) {
List<Text> putDataHere = [];
final things = snapshot.data.docs;
for (var thing in things) {
final myData = Map<String, String>.from(thing.data());
final myThing = myData['dataTitle'];
final thingWidget = Text(myThing);
putDataHere.add(thingWidget);
}
return Expanded(
child: ListView(
children: putDataHere,
),
);
},
);
You can try this:
Firestore.instance
.collection("things")
.orderBy('createdAt', descending: true or false).getDocuments()
And then you can store createdAt on your client side with Timestamp, and you can get current timestamp with Timestamp.now()
Since the expected behaviour is that ListView items are ordered based on the server timestamp, you can just sort the list after you've gotten it from Firestore.
final things = snapshot.data.docs;
things.sort((a, b) {
return (a['timestamp'] as Timestamp).compareTo(b['timestamp'] as Timestamp);
});
The problem was totally different from what I had initially thought.
I honestly forgot that when I retrieve the data from the Firestore to process it, I set my Map to Map<String, String>, it was fine before because I only had String, but now that I have Timestamp type, it doesn't work.
The answer to the question is just simply changing the Map<String, String> to Map<String, dynamic>
I'm building a task management app where every user has multiple projects (collections), inside every project there are tasks (collections), and inside every task there are sub-tasks.
I was able to retrieve all the projects using this code:
User user = Provider.of<User>(context);
CollectionReference projects = Firestore.instance.collection('users').document(user.uid).collection("projects");
But my question is how can I get all the tasks and sub-tasks (if they exist) from every project and list them in a StreamBuilder?
Here's my current code:
Flexible(
child: StreamBuilder<QuerySnapshot>(
stream: projects.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return new ListView(
children:
snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document.data['project_name']),
subtitle: new Text(document.data['project_description']),
);
}).toList(),
);
},
),
),
If the project has tasks I want to display them below the project description, and if the task has sub-tasks I want to show them below its parent task.
It sounds like you're essentially asking how to list nested subcollections, which is not possible for web and mobile clients. See also:
How to list subcollections in a Cloud Firestore document
How to list subcollection on firestore?
Also, all Firestore queries are shallow, meaning they only consider documents in one collection at a time. There are no deep or recursive queries.
Consider instead maintaining the list of projects in a document that you can get() directly. You can use that list to then query each subcollection directly.
There are different kind of implementations for this scenario, basically subcollections are located under one single document, the query identifies a group of documents, when you know that the query only identifies one document, the Firestore client doesn't know that, and even if it did, it doesn,t know the full path for that document.
You need to execute a query for getting individual documents, and then you can get the subcollection of each one.
You can find a good example using flutter here.
I had a similar issue, and used bloc provider alongside streams,
I first created a model for Json parsing (In my case I had an app whereby users can create campaigns, these campaigns were saved as a sub-collection in the UserData collection, the document IDs were the userID from FirebaseAuth(now User in flutter migration). The objective was to loop through UserData collection for each user's subcollection (UserCampaigns) and retrieve all of them and display in the UI with bloc. userCampaignRepository:
List<UserCampaignModel> UserCampaignList = [];
CreateListofAllUserCampaigns(QuerySnapshot snapshot) async {
var docs = snapshot.docs;
for (var Doc in docs) {
UserCampaignList.add(
UserCampaignModel.fromEntity(UserCampaignEntity.fromSnapshot(Doc)));
}
return UserCampaignList;
}
Stream<List<UserCampaignModel>> getListUserCampaigns() async* {
final collection = FirebaseFirestore.instance.collection('UserData');
final List list_of_users = await collection.get().then((value) => value.docs);
for (int i = 0; i < list_of_users.length; i++) {
FirebaseFirestore.instance.collection("UserData")
.doc(list_of_users[i].id.toString())
.collection("UserCampaigns")
.snapshots()
.listen(CreateListofAllUserCampaigns);
}
yield UserCampaignList;
}
In the bloc: I have 2 events, one to initiate the Load of Events by listening to the stream and calling the loader event.
if (event is InitiateLoadOfAllUserCampaignsEvent) {
yield AllUserCampaignsState.initial();
try {
_userCampaignSubscription?.cancel();
_userCampaignSubscription = _userCampaignRepository
.getListUserCampaigns()
.listen((allCampaigns) {
print("Load event is called");
add(
LoadAllUserCampaignsEvent(allUserCampaigns: allCampaigns),
);
});
} catch (e) {
print("error is ${e}");
yield AllUserCampaignsState.allUserCampaignLoadFailure();
}
} else if (event is LoadAllUserCampaignsEvent) {
yield* _mapLoadAllUserCampaignsEventToState(event);
}}}
Before you say this is a duplicate question or that I should use nested stream builders, please hear me out.
I am designing a social media type application. And I want users to receive updates whenever someone they are following posts something in their "my Followers Posts" collection.
In the app, the app will check firebase rtdb for the current user's following list(the people he is following) and make a list of their uids.
I plan on using said list to create a list of streams (ordered by time, of course) and merge them into one stream that shall then be fed into a stream builder on the private feed page.
On this page, the user will be able to easily follow what their people of interest have been posting.
I figured such a system is a lot more cost efficient than every user having a document in the "private Feed" collection and whenever someone posts something, the app reads their list of followers and then promptly posts an update in each and every one of their private feeds.
Because... Picture someone with 2 million followers. That's 2 million writes instantly. And later on, 2 million reads.
I figured it's a lot more cost efficient for the poster to just put the post in their "publicFeed" and the different followers simply listen in onto that feed and keep up to tabs with them.
But.. This requires implementing a merging of multiple streams (more than 2). How do I do this?
I have tried reading into RxDart but it is total Greek to me. I am relatively a beginner in dart. I've only been coding for about 5 months now.
You can use StreamGroup from the async package: https://pub.dev/documentation/async/latest/async/StreamGroup-class.html to group events from multiple streams - it's well documented and maintained by the dart team. If you have no RxDart experience this is a good choice. It does not have all the features of rx but for a beginner it should be easier to wrap your head around this
I recently had a similar case, and what I suggest You to do is this
(I'm using cloud firestore, but I'm sure You have your streams already written so the important part is the usage of multiple streams):
You have to add this plugin to pub spec.yaml:
https://pub.dev/packages/rxdart
Here is the repository for (in your case posts, let's say newPosts, oldPosts):
class PostRepository {
static CollectionReference get collection => yourCollectionRef;
static Stream<List<Post>> newPosts() {
Query query = collection
.where('Your condition like was viewed', isEqualTo: false)
.orderBy('updateDate', descending: true)
.limit(50);
return query.snapshots().map<List<Post>>((querySnapshot) {
final _newPosts = querySnapshot.documents.map((doc) {
final post = Post.fromDoc(doc);
return post;
}).where((p) => p != null);
return _newPosts
});
}
static Stream<List<Post>> oldPosts() {
Query query = collection
.where('Your condition like was viewed', isEqualTo: true)
.orderBy('updateDate', descending: true)
.limit(50);
return query.snapshots().map<List<Post>>((querySnapshot) {
final _oldPosts = querySnapshot.documents.map((doc) {
final post = Post.fromDoc(doc);
return post;
}).where((p) => p != null);
return _oldPosts
});
}
}
Then to get multiple streams (those two from above combined), do like this in your widget class:
IMPORTANT! you have to import this - import 'package:rxdart/streams.dart';
List<Post> newPosts;
List<Post> oldPosts;
Widget _pageContent() {
return SingleChildScrollView(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: newPosts.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(newPosts[index].title)
);
}
),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: oldPosts.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(oldPosts[index].title)
);
}
)
]
)
);
}
Widget _posts() {
return StreamBuilder(
stream: CombineLatestStream.list([
PostRepository.getNewPosts(),
PostRepository.getOldPosts()
]),
builder: (context, snap) {
if (snap.hasError) {
debugPrint('${snap.error}');
return ErrorContent(snap.error);
} else if (!snap.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
newPosts = snap.data[0];
oldPosts = snap.data[1];
return _pageContent();
}
);
}
I wrote the code like from head, so there may be some small errors, but I hope You get the point, enjoy :)
Scenario explained:
Using: import 'package:cloud_firestore/cloud_firestore.dart';
Cloud Firestore:
a Collection 'sessionsLog', containing documents with the following fields:
'sessionId' a String
'isClosed' a bool
'locationId' a String and some other fields ...
I want to retrieve all documents with a false bool for 'isClosed', only once, and then perform some actions. Therefore I chose to use a FutureBuilder (not a streambuilder), what I did:
FutureBuilder(
future: Firestore.instance.collection('sessionsLog')
.where('isClosed', isEqualTo: false).getDocuments();
builder: (ctx, sessionSnapshot) {
return RaisedButton(
child: Text('Search'),
onPressed: () async {
//some other code here, that needs async I added on top
if (sessionSnapshot.connectionState !=
ConnectionState.done) {
return CircularProgressIndicator();
}
if (!sessionSnapshot.hasData) {i should
print('nothing to show here');
}else {
//the below line of code is wrong, it could be print / or Text / or any other widget
//I've just added it for the sake of an example.
print(sessionSnapshot.data.documents[0]['parkingLocationName']);}});}),
The above code is not working, I've just created it for a simple code result, to just be able to print values I am retrieving, so please don't fix something in my code.
Also noting that, in my code logic, there will always be once single document with 'isClosed' = false. So, I should always receive one single document.
But If it differs, please give me 2 examples, one if retrieved data contains 1 single document and another if there could be several documents and therefore how would I iterate in the data.
I just want the correct way to be able to READ/access the data I am receiving, whether to print the fields info, or display them in a text. anything, but how would I do that, in the exact explained above scenarios.
Would really appreciate a simple sample example based on my code scenario.
You could do it in a future like this:
Future<void> performActionsOnClosed() async {
await FirebaseFirestore.instance
.collection('sessionsLog')
.where('isClosed', isEqualTo: false)
.get()
.then(
(snapshot) {
if (snapshot.docs.isNotEmpty) {
// Perform actions
} else {
print('No Documents Found');
}
},
);
}