streaming and transforming a Firestore document snapshots() stream - flutter

I am trying to get a document stream from Firestore, to extract one of its fields in my repository and to stream the field to my bloc. I use return orderBriefDoc.snapshots().map((snapshot) { for this.
However, upon first call, no internal map instruction becomes executed and instead I receive a type mismatch type '_MapStream<DocumentSnapshot, dynamic>' is not a subtype of type 'Stream<List<OrderBrief>>'. I do not understand why the return type of the .map() method does not depend on what I return within its return statement and why this internal code is not executed.
First of all, I used the repository function of Felix Angelov's Firebase authentication ant of the todo list as a blueprint:
Stream<User> get user {
return _firebaseAuth.authStateChanges().map((firebaseUser) {
return firebaseUser == null ? User.empty : firebaseUser.toUser;
});
}
Stream<List<Todo>> todos() {
return todoCollection.snapshots().map((snapshot) {
return snapshot.documents
.map((doc) => Todo.fromEntity(TodoEntity.fromSnapshot(doc)))
.toList();
});
}
My adaption looks like this
#override
Stream<List<OrderBrief>> orderBriefs() {
if (orderBriefDoc == null)
getOrderCollection();
return orderBriefDoc.snapshots().map((snapshot) {
final tmp = snapshot;
print ("2");
final tmp2 = snapshot.data()['orderBriefs'];
print("3");
return snapshot.data()['orderBriefs'].map((orderBrief) {
final tmp=orderBrief;
final tmp2 = OrderBriefEntity.fromMap(orderBrief);
final tmp3 = OrderBrief.fromEntity(tmp2);
return tmp3;
}).toList();
});
}
For some reason "2" and "3" are not printed upon first call, and due to the type mismatch the app execution fails. So in my function orderBriefs() I return a .map() of a snapshots() stream. The mapped value, so the single document snapshot is mapped again to extract the orderBriefs field. This field is transformed from an storage entity class OrderBriefEntity to my business logic class OrderBrief. The transformation result is the final return value. Hence I would expect the function orderBriefs() to return a list stream of this transformation result. However, a _MapStream<DocumentSnapshot, dynamic> is returned. Why?
PS: This refers to my question, but with a slightly different angle

So, finally I found a method to stream a single document of Firestore. I finally had the idea to look up the documentation of Stream and found a working example there. Why it does not work with only the map method like Felix did it, no idea. That being said, I still follow his pattern to transform the snapshot to an "entity" and then further to the data structure used by the bloc and the ui.
I finally needed to flavors, stream a single field (nested array) of a document and stream a whole document
(1) Streaming a field of a document.
#override
Stream<List<OrderBrief>> orderBriefs() async* {
if (orderBriefDoc == null)
getOrderCollection();
Stream<DocumentSnapshot> source = orderBriefDoc.snapshots();
await for (var snapshot in source) {
final List<OrderBrief> returnVal = snapshot.data()['orderBriefs'].map<OrderBrief>((orderBrief) {
return OrderBrief.fromEntity(OrderBriefEntity.fromMap(orderBrief));
}).toList();
yield returnVal;
}
}
(2) Streaming a document with all of its fields
#override
Stream<Order> orderStream(String orderId) async* {
Stream<DocumentSnapshot> source = orderBriefDoc.collection('orders').doc(orderId).snapshots();
await for (var snapshot in source) {
final Order returnVal = Order.fromEntity(OrderEntity.fromSnapshot(snapshot));
yield returnVal;
}
}
How to neatly integrate this with a listener in the bloc, you will find in the Firestore ToDos example at bloclibrary

Related

Return multiple value from function in dart

Hi so i'm new to dart and I'm having an issue with returning 2 value from a dart function.
Currently I have this function :
Future LoadAllData({required Map data, required String detailId}) async {
loadUserData(data: data);
powData = await Database.getPowDataActive(detailId: detailId);
return powData;
}
so getPowDataActive is a function that will fetch a data from my database and it will return some map data, load user data will also fetch data and will also return some map. I wanted to use the loadAllData function for my futureBuilder and use the snapshot data from this 2 function for different purposes, can I do so ? or I have to first combine the return from both function into 1 variable and access it differently ?
Thanks before
You can create a model like this:
class LoadDataResult {
final Map userData;
final Map powData;
LoadDataResult({#requierd this.userData, #requierd this.powData, });
}
and then use it like this:
Future<LoadDataResult> LoadAllData({required Map data, required String detailId}) async {
var userData = await loadUserData(data: data);
powData = await Database.getPowDataActive(detailId: detailId);
return LoadDataResult(userData:userData, powData: powData);
}
and then use it like this in futureBuilder:
LoadDataResult data = snapshot.data;
print('${data. userData}');

Problem while reading document from firestrore in flutter

I am new to Firebase I want to read all data of document here is how i am trying to read
This is my function to get data.
Future<List<AgencyModel>> getAgencyData() async {
List<AgencyModel> agencyListModel = [];
try {
agencyListModel = await _db.collection('colelctionName')
.doc('myDoc')
.snapshots()
.map((doc)=> AgencyModel.fromJson(doc.data()!)).toList();
print('List : ${agencyListModel.length}');
return agencyListModel;
} catch (e) {
debugPrint('Exception : $e');
rethrow;
}
}
This is how i am calling the above function
getAgencyDetails() async {
List<AgencyModel> data = await fireStoreService.getAgencyData();
print('Data : ${data.first}');}
and this is my models class fromjson function
factory AgencyModel.fromJson(Map<String, dynamic> json) {
return AgencyModel(
agencyName: json['agencyName'],
agencyContact: json['agencyContact'],
agencyAddress: json['agencyAddress'],
cnic: json['cnic'],
agencyContactDetails: json['agencyContactDetails'],
certificatesUrl: json['certificatesUrl'],
locationUrl: json['locationUrl'],
earning: json['earning'],
processing: json['processing'],
onHold: json['onHold']);}
I am not getting any error or exception, Also these two print statements does not work not display anything not even the Strings List : and Data : i.e
print('List : ${agencyListModel.length}');
print('Data : ${data.first}');}
UPDATE:
According to the documentation:
https://firebase.google.com/docs/firestore/query-data/get-data#dart_1
It is necessary to distinguish whether you want to retrieve the data only once or listen to changes over the document in real time.
It seems to me like you want to accomplish 1. case that you only want to retrieve data once. In that case.
You should change:
agencyListModel = await _db.collection('collectionName')
.doc('myDoc')
.snapshots()
agencyListModel = await _db.collection('collectionName')
.doc('myDoc')
.get()

Get data from firestore and convert to json and use that in factory constructor

I am using a firestore as database. If the data not present in database then it will do webscrape. In webscrape I managed to convert the data into json and used factory constructor. I want the same thing should happen while fetching data from firestore.
One More thing I have a specific collection and document id So I used .collection('medicine').doc('id').get().
Factory Constructor
class Tablet {
String name;
String introduction;
Tablet({
this.name,
this.introduction,
});
factory Tablet.fromJson(Map<String, dynamic> json) {
return Tablet(
name: json['name'],
introduction: json['introduction'],
);
}
}
Method which get Data from Database If Data Present
This is a Method which must returns Future<Tablet> (I stuck in if condition)
Future<Tablet> details;
Future<Tablet> getDetails(String medName) async {
await Firebase.initializeApp();
await FirebaseFirestore.instance
.collection('medicine')
.doc(medName.toLowerCase())
.get()
.then((DocumentSnapshot docSnapshot) {
if (docSnapshot.exists) {
var json = jsonDecode(docSnapshot.data().toString()); // I am getting stuck here
details = Tablet.fromJson(json) as Future<Tablet>;
} else {
print('Data not present in Database..');
details = webScrape(medName);
}
});
return details;
}
What I Tried in webscrape(medName) is
Tablet.fromJson(jsonDecode(response.body));
Here returning above line and assigning it to Future works but in the if condition, It is asking for type cast and the following error thrown
Error
FormatException: Unexpected character (at character 2)
{benefits: {values: [{header: In Heartburn, display_text: Heartburn and aci...
^
Update
As #Tarik Huber 2nd suggestion, Inspired and changed a code little bit as shown
factory Tablet.fromSnapshot(DocumentSnapshot docSnap){
return Tablet(
name: docSnap.get('name');
introduction: docSnap.get('introduction');
);
}
Now My Question is, the below code isn't working
details = Tablet.fromSnapshot(docSnapshot.data()) as Future<Tablet> // is not working
instead calling async function did as shown
details = arrangeData(docSnapshot.data());
Future<Tablet> arrangeData(DocumentSnapshot data) async{
return Tablet.fromSnapshot(data);
}
I know It's not a good approach and It is working but don't know How? Can anyone Explain..
Have you tried it with:
details = Tablet.fromJson(docSnapshot.data()) as Tablet;
the data is of type Map<String,dynamic>. That is the same type your convertion functions takes.
Othervise you could just add a
factory Tablet.fromSnapshot(DocumentSnapshot docSnapshot) {
return Tablet(
name: docSnapshot.get('name'),
introduction: docSnapshot.get('introduction'),
);
}

Flutter pagination with firestore stream

How to properly implement pagination with firestore stream on flutter (in this case flutter web) ?
my current approach with bloc which is most likely wrong is like this
function called on bloc when load next page, notice that i increased the lastPage variable of the state by 1 each time the function is called:
Stream<JobPostingState> _loadNextPage() async* {
yield state.copyWith(isLoading: true);
try {
service
.getAllDataByClassPage(state.lastPage+1)
.listen((List<Future<DataJob>> listDataJob) async {
List<DataJob?> listData = [];
await Future.forEach(listDataJob, (dynamic element) async {
DataJob data= await element;
listData.add(data);
});
bool isHasMoreData = state.listJobPostBlock.length!=listData.length;
//Update data on state here
});
} on Exception catch (e, s) {
yield StateFailure(error: e.toString());
}}
function called to get the stream data
Stream<List<Future<DataJob>>> getAllDataByClassPage(
String className, int page) {
Stream<QuerySnapshot> stream;
if (className.isNotEmpty)
stream = collection
.orderBy('timestamp', "desc")
.where('class', "==", className).limit(page*20)
.onSnapshot;
else
stream = collection.onSnapshot;
return stream.map((QuerySnapshot query) {
return query.docs.map((e) async {
return DataJob.fromMap(e.data());
}).toList();
});
}
With this approach it works as intended where the data loaded increased when i load next page and still listening to the stream, but i dont know if this is proper approach since it replace the stream could it possibly read the data twice and end up making my read count on firestore much more than without using pagination. Any advice is really appreciated, thanks.
Your approach is not very the best possible indeed, and as you scale you going to be more costly. What I would do in your shoes would be to create a global variable that represents your stream so you can manipulate it. I can't see all of your code so I am going to be as generic as possible so you can apply this to your code.
First let's declare the stream controller as a global variable that can hold the value of your stream:
StreamController<List<DocumentSnapshot>> streamController =
StreamController<List<DocumentSnapshot>>();
After that we need to change your getAllDataByClassPage function to the following:
async getAllDataByClassPage(String className) {
Stream stream = streamController.stream;
//taking out of the code your className logic
...
if(stream.isEmpty){
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.limit(20)
.onSnapshot
streamController.add(snap.docs);
}else{
DocumentSnapshot lastDoc = stream.last;
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.startAfterDocument(lastDoc)
.limit(20)
.onSnapshot;
streamController.add(snap.docs);
}
}
After that all you need to do in order to get the stream is invoke streamController.stream;
NOTE: I did not test this code but this is the general ideal of what you should try to do.
You can keep track of last document and if has more data on the list using startAfterDocument method. something like this
final data = await db
.collection(collection)
.where(field, arrayContains: value)
.limit(limit)
.startAfterDocument(lastDoc)
.get()
.then((snapshots) => {
'lastDoc': snapshots.docs[snapshots.size - 1],
'docs': snapshots.docs.map((e) => e.data()).toList(),
'hasMore': snapshots.docs.length == limit,
});

How do I load a single entry in Sembast based on it's ID?

I look through a bunch of tutorials for Sembast. The tutorials give examples about how to get all the items of a certain class like Cake but not how to load a specific one based on it's ID:
#override
Future<List<Cake>> getAllCakes() async {
final snapshots = await _store.find(_database);
return snapshots
.map((snapshot) => Cake.fromMap(snapshot.key, snapshot.value))
.toList(growable: false);
}
How do I lot a single Cake for a given ID?
You can use store.record(key).get(db)
Assuming your key is an int, this could look like this:
Future<Cake> getCake(int key) async {
final snapshot = await _store.record(key).getSnapshot(_database);
return snapshot != null ? Cake.fromMap(key, snapshot.value) : null;
}
More information (yes documentation is not that great) here