Perform stream calculations in app "brain" - An alternative to Stream Builder - firebase

I am listening to a number of streams with RxDart. I then use a StreamBuilder to process the data as it comes in and update String and int variables that I then display in my app. Streams and variables are displayed below.
Stream<QuerySnapshot> janFlightsStream(BuildContext context) async* {
final uid = await Provider.of(context).auth.getCurrentUID();
yield* Firestore.instance
.collection("UserAirTravelInputs")
.document(uid)
.collection(formatDate(DateTime.now(), [yyyy, '', '']))
.where("entryMonth", isEqualTo: "Jan")
.snapshots();
}
Stream<QuerySnapshot> febFlightsStream(BuildContext context) async* {
final uid = await Provider.of(context).auth.getCurrentUID();
yield* Firestore.instance
.collection("UserAirTravelInputs")
.document(uid)
.collection(formatDate(DateTime.now(), [yyyy, '', '']))
.where("entryMonth", isEqualTo: "Feb")
.snapshots();
}
double janFlightsKm;
int janNumberOfEntries;
StreamBuilder returns a widget which is fine here. But I do this on each screen in my app which is extremely inefficent. I want to extract this logic (of listening to firebase stream data and making calculations based on that data) from each screen to something like a provider or app "brain" which handles this data, so that I only need tp access the output of these calculations when needed in the different app routes.
StreamBuilder<List>(
stream: CombineLatestStream.list([
janFlightsStream(context),
febFlightsStream(context),
]),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SpinKitDoubleBounce(
color: kDarkBlue,
size: 100,
),
Text("Loading data..."),
],
),
);
}
if (snapshot.data[0].documents.length != 0) {
final janFlights = snapshot.data[0].documents;
double janTotalGCO2e = 0;
janNumberOfEntries = snapshot.data[0].documents.length;
for (var flight in janFlights) {
double janKm = flight.data["flightKm"];
janTotalGCO2e += janKm;
}
janFlightsKm =
double.parse((janTotalGCO2e / 1000).toStringAsFixed(2));
} else {
janFlightsKm = 0;
janNumberOfEntries = 0;
}
if (snapshot.data[1].documents.length != 0) {
//Do something
} // Do something else
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(),
body: Container(
child: Column(
children: [
Text(janNumberOfEntries.toString()),
Text(janFlightsKm.toString()),
],
),
),
);
}),
Is there a way to do this without using this repeated StreamBuilder? I have looked in to StreamProvider but appears that only gives the stream of data, and I would then have to process the data again each time I tap in to the StreamProvider which does not solve the problem.
Simply it is 1. list of streams. 2. process data coming from streams (as code above), 3. store that data, and 4. make it available throughout the app.
Any help would be greatly appreciated!

Related

Flutter Firestore - get only 10 docs with their id

here I'm trying to get the top 10 names only from my firebase firestore,
and I searched on how I do it with the listview that I have, but I get to nowhere.
so I thought about getting the id of my documents instead.
In my firestore I gave the top 10 documents IDs from 1 to 10, now I'm stuck and I have no idea how to do it. please help.
static int id = 1;
StreamBuilder<QuerySnapshot>(
stream: fireStore.collection(path).snapshots(),
builder: (context, snapshot) {
if(!snapshot.hasData){
return Center(
child: CircularProgressIndicator(
backgroundColor: Colors.blueAccent,
),
);
}
List<InfoBox> ancients = [];
try {
final collection = snapshot.data!.docs;
for (var collect in collection) {
final name = collect['Name'];
final image = collect['Image'];
final collectionWidget = InfoBox(
ancientName: name,
ancientImage: image
);
ancients.add(collectionWidget);
}
}catch(e){
print('problems in stream builder \n error : $e');
}
return Expanded(
child:ListView(
children: ancients,
)
);
},
);
You are probably searching for limit() function. According to the documentation:
To limit the number of documents returned from a query, use the limit method on a collection reference
You can implement it like this:
fireStore.collection(path).limit(10).snapshots();
Change your ListView to this List if else everything is ok and u are getting data in list then this will work.
ListView.builder(
itemCount: ancients.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: new Image.network(ancients[index].ancientImage),
title: new Text(ancients[index].ancientName),
)
},
)

Can't get actual String download url from Firebase Storage and only returns Instance of 'Future<String>' even using async/await

I am trying to get user avatar from firebase storage, however, my current code only returns Instance of 'Future<String>' even I am using async/await as below. How is it possible to get actual download URL as String, rather Instance of Future so I can access the data from CachedNewtworkImage?
this is the function that calls getAvatarDownloadUrl with current passed firebase user instance.
myViewModel
FutureOr<String> getAvatarUrl(User user) async {
var snapshot = await _ref
.read(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
if (snapshot != null) {
print("avatar url: $snapshot");
}
return snapshot;
}
getAvatarURL is basically first calling firebase firestore reference then try to access to the downloadURL, if there is no user data, simply returns null.
Future<String> getAvatarDownloadUrl(String code) async {
Reference _ref =
storage.ref().child("users").child(code).child("asset.jpeg");
try {
String url = await _ref.getDownloadURL();
return url;
} on FirebaseException catch (e) {
print(e.code);
return null;
}
}
I am calling these function from HookWidget called ShowAvatar.
To show current user avatar, I use useProvider and useFuture to actually use the data from the database, and this code works with no problem.
However, once I want to get downloardURL from list of users (inside of ListView using index),
class ShowAvatar extends HookWidget {
// some constructors...
#override
Widget build(BuildContext context) {
// get firebase user instance
final user = useProvider(accountProvider.state).user;
// get user avatar data as Future<String>
final userLogo = useProvider(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
// get actual user data as String
final snapshot = useFuture(userLogo);
// to access above functions inside of ListView
final viewModel = useProvider(myViewModel);
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: snapshot.data, // **this avatar works!!!** so useProvider & useFuture is working
),
),
SizedBox(height: 32),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Center(
child: Column(
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: viewModel
.getAvatarUrl(goldWinners[index].user)
.toString(), // ** this avatar data is not String but Instance of Future<String>
),
),
),
],
),
);
},
itemCount: goldWinners.length,
),
Avatar() is simple statelesswidget which returns ClipRRect if avatarURL is not existed (null), it returns simplace placeholder otherwise returns user avatar that we just get from firebase storage.
However, since users from ListView's avatarUrl is Instance of Future<String> I can't correctly show user avatar.
I tried to convert the instance to String multiple times by adding .toString(), but it didn't work.
class Avatar extends StatelessWidget {
final String avatarUrl;
final double radius;
final BoxFit fit;
Avatar({Key key, this.avatarUrl, this.radius = 16, this.fit})
: super(key: key);
#override
Widget build(BuildContext context) {
print('this is avatar url : ' + avatarUrl.toString());
return avatarUrl == null
? ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image.asset(
"assets/images/avatar_placeholder.png",
fit: fit,
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: CachedNetworkImage(
imageUrl: avatarUrl.toString(),
placeholder: (_, url) => Skeleton(radius: radius),
errorWidget: (_, url, error) => Icon(Icons.error),
fit: fit,
));
}
}
Since the download URL is asynchronously determined, it is returned as Future<String> from your getAvatarUrl method. To display a value from a Future, use a FutureBuilder widget like this:
child: FutureBuilder<String>(
future: viewModel.getAvatarUrl(goldWinners[index].user),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return snapshot.hashData
? Avatar(avatarUrl: snapshot.data)
: Text("Loading URL...")
}
)
Frank actually you gave an good start but there are some improvements we can do to handle the errors properly,
new FutureBuilder(
future: //future you need to pass,
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, i) {
DocumentSnapshot ds = snapshot.data.docs[i];
return //the data you need to return using /*ds.data()['field value of doc']*/
});
} else if (snapshot.hasError) {
// Handle the error and stop rendering
GToast(
message:
'Error while fetching data : ${snapshot.error}',
type: true)
.toast();
return new Center(
child: new CircularProgressIndicator(),
);
} else {
// Wait for the data to fecth
return new Center(
child: new CircularProgressIndicator(),
);
}
}),
Now if you are using a text widget as a return statement in case of errors it will be rendered forever. Incase of Progress Indicators, you will exactly know if it is an error it will show the progress indicator and then stop the widget rendering.
else if (snapshot.hasError) {
}
else {
}
above statement renders until, if there is an error or the builder finished fetching the results and ready to show the result widget.

How can I Paginate Firestore Data in ListView.Builder and Still Get Realtime Updates in Flutter?

I can't figure out how to paginate Firestore data without breaking the realtime listener. The data is passed to a StreamBuilder and displayed in a ListView.builder. I'm trying to fetch the next set of data when the user reaches maxScrollExtent.
I understand how to to use startAfter and limit with Firestore to paginate. If I pass the fetched data to a StreamController and use that in the StreamBuilder, pagination works fine but Firestore doesn't send any updates to the device.
If I pass Firestore.instance.(...).snapshots() directly to the StreamBuilder (without using a StreamController), then I get updates from the server, but pagination is all screwed up. The UI is rebuilt and I'm sent to the top of the list.
Using StreamController
final int limit = 2;
final _list = List<DocumentSnapshot>();
final _listController = StreamController<List<DocumentSnapshot>>.broadcast();
DateTime startAt;
bool _isAllDataFetched = false;
Stream<List<DocumentSnapshot>> get listStream => _listController.stream;
void initState() {
super.initState();
_eventDao
?.getAllEventMedia(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit)
?.then((QuerySnapshot querySnapshot) {
_list.addAll(querySnapshot.documents);
_listController.sink.add(_list);
})?.catchError((error) {
print(error);
});
}
Widget build(BuildContext context) {
return StreamBuilder<List<DocumentSnapshot>>(
stream: listStream,
builder: (BuildContext context, AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (snapshot.hasError) {
return ErrorPage(imageAsset: AssetResources.failed);
}
if (snapshot.hasData &&
snapshot.data.isNotEmpty &&
snapshot.connectionState == ConnectionState.active) {
startAt = DateTime.fromMillisecondsSinceEpoch(
snapshot.data.last.data[EventMediaSchema.creationDate],
);
return MediaDisplayManagerAlt(
documents: snapshot.data,
atBottom: _fetchNextDocumentSet,
);
}
if (snapshot.hasData && snapshot.connectionState == ConnectionState.active) {
_isAllDataFetched = true;
}
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text("NO MEDIA YET"),
),
],
),
);
},
);
void _fetchNextDocumentSet() async {
if (!_isAllDataFetched) {
QuerySnapshot querySnapshot = await _eventDao?.getAllEventMedia(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit);
_list.addAll(querySnapshot.documents);
_listController.sink.add(_list);
}
}
Passing snapshots to StreamBuilder
final int limit = 2;
DateTime startAt;
bool _isAllDataFetched = false;
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _eventDao?.(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return ErrorPage(imageAsset: AssetResources.failed);
}
if (snapshot.hasData && snapshot.data.documents.isNotEmpty) {
return MediaDisplayManagerAlt(
documents: snapshot.data.documents,
atBottom: _fetchNextDocumentSet,
);
}
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text("NO MEDIA YET"),
),
],
),
);
},
);
}
void _fetchNextDocumentSet() async {
if (!_isAllDataFetched) {
QuerySnapshot querySnapshot = await _eventDao?.getAllEventMedia(event?.id ?? "",
startAfter: startAt?.millisecondsSinceEpoch, limit: limit);
_list.addAll(querySnapshot.documents);
_listController.sink.add(_list);
}
}
It seems like these two approaches need to be combined in some way to get the desired effect, but after two days of trying I can't figure how to do that.

In Dart/Flutter, how can I find out if there is no Collection in the Firestore database?

I'd like to check if a collection (the users collection) exists in the Firestore database or not. But I cannot find any means of doing that. If the collection exists, I would like to stream its documents, otherwise an empty stream as you see in the following method
- Is there any way to find a collection exists without getting its snapshots?
- Why break; or yield* Stream.empty() hangs the stream, like an endless stream!
Stream<userInfo> getCurrentUserInfos() async* {
final String usersCollectionPath = "users";
Stream<QuerySnapshot> snapshots = Firestore.instance.collection(usersCollectionPath).snapshots();
snapshots.isEmpty.then((hasNoUserCollection) {
// Although I don't have 'users' collection
// in my database but I never reach this point!
// i.e. hasNoUserCollection is always FALSE!
if(hasNoUserCollection) {
print("users collection doesn't exist, create it!");
// next line (i.e. `break;`) hangs the tool!
// And sometimes hangs the VSCode's IDE as well, if I have a breakpoint on it!!!
break;
// I tried yielding an empty stream instead of break, again another hang!
// yield* Stream<userInfo>.empty();
} else {
// previous stream is dead, so create it again!
snapshots = Firestore.instance.collection(usersCollectionPath ).snapshots();
await for (QuerySnapshot snap in snapshots) {
for (DocumentSnapshot userDoc in snap.documents) {
yield (new userInfo.fromQuerySnapshot(userDoc));
}
}
});
}
Now even a try-catch block cannot catch what's gone wrong, when the stream is empty!
try{
getCurrentUserInfos().last.then((userInfolastOne) {
print("last one: $lastOne.name");
});
// the following line (i.e. await ...) at least doesn't hang and
// `catch` block is able to catch the error `Bad state: No element`,
// when the stream is empty
//
// userInfo lastOne = await stream.last;
} catch (ex) {
print ("ex: $ex");
}
There is no API to detect if a collection exists. In fact: a collection in Firestore only exists if there are documents in it.
The cheapest check I can think of is doing a query for a single document, and then checking if that query has any results.
Okay, maybe I figured it out
final snapshot = await Firestore.instance.collection(collectionName).getDocuments();
if (snapshot.documents.length == 0) {
//Doesn't exist
}
This worked for me
As stated by #Frank, a collection in Firestore gets deleted if no Documents exist in it.
However, I understand that there might be cases where you want to keep a history of the collection modification/ creation events, or let's say for some reason prevent Collections from being deleted.
Or for example, you want to know when a collection was created in the first place. Normally, if the Documents are deleted, and then the Collection gets created again, you will not know the initial creation date.
A workaround I can think of is the following:
Initialize each collection you want with a Document that will be specifically for keeping generic info about that collection.
For example:
This way, even if all other Documents in the Collection are deleted, you'll still keep the Collection in addition to some info that might be handy if In the future you need to get some history info about the Collection.
So to know if a Collection exists of no, you can run a query that checks for a field in Info Documents (eg CollectionInfo.exists) to know which Collections have been already created.
This is a sample from one of my projects. You can use snapshot.data!.docs.isEmpty to check if a collection has data or not.
StreamBuilder(
stream: _billGroupStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('Something went wrong'),
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
children: const [
LinearProgressIndicator(),
Text('Loading data, please wait...'),
],
),
);
} else if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.data!.docs.isEmpty) {
return const Center(
child: Text(
'Huh!! Looks like you have no transactions yet!' ,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
);
} else if (snapshot.connectionState == ConnectionState.active) {
final List<DocumentSnapshot> docs = snapshot.data!.docs;
return ListView.builder(
shrinkWrap: true,
restorationId: 'billModeList',
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
///This document snapshot is used for determining the unique id for update and delete methods
final DocumentSnapshot doc = docs[index];
//final DocumentSnapshot doc = snapshot.data!.docs[index];
///This [BillModel] converted data is used to build widgets
final BillModel billModel = doc.data()! as BillModel;
return Dismissible(
onDismissed: (direction) {
_remoteStorageService.deleteItemFromGroup(
widget.uri, doc.id);
setState(() {
docs.removeAt(index);
});
},
background: Container(
color: Colors.red,
child: const Icon(Icons.delete_forever_sharp),
),
key: Key(doc.id),
child: Card(
elevation: 3,
shadowColor: Colors.teal.withOpacity(.5),
child: ListTile(
leading:
const CircleAvatar(child: Icon(Icons.attach_money)),
title: Text(billModel.name),
subtitle: Text(billModel.category ?? ''),
trailing: Text(billModel.amount.toString()),
),
),
);
},
);
}
return const CircularProgressIndicator();
},
),

Flutter merge two firestore streams into a single stream

I simply want to perform an 'OR' operation and get the both results of two queries into one stream.
Here's my code with a single stream
StreamBuilder(
stream: Firestore.instance
.collection('list')
.where('id', isEqualTo: 'false')
.orderBy('timestamp')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: CircularProgressIndicator(),
)
],
);
if (snapshot.data.documents.length == 0)
return const Center(
child: Text(
"Not Available",
style: TextStyle(fontSize: 30.0, color: Colors.grey),
),
);
return ListView.builder(
padding: EdgeInsets.all(5.0),
key: Key(randomString(20)),
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
return ListCard(snapshot.data.documents[index]);
},
);
}),
Instead of a single stream now I want to feed two stream to the same stream builder.
I tried StreamGroup but it's not working since Widgets rebuild
StreamGroup.merge([streamOne, streamTwo]).asBroadcastStream();
I tried followed method also
Stream<List<DocumentSnapshot>> searchResult() {
List<Stream<List<DocumentSnapshot>>> streamList = [];
Firestore.instance
.collection('room-list')
.where('id', isEqualTo: 'false')
.snapshots()
.forEach((snap) {
streamList.add(Observable.just(snap.documents));
});
Firestore.instance
.collection('room-list')
.where('id', isEqualTo: 'pending')
.snapshots()
.forEach((snap) {
streamList.add(Observable.just(snap.documents));
});
var x = Observable.merge(streamList)
.scan<List<DocumentSnapshot>>((acc, curr, i) {
return acc ?? <DocumentSnapshot>[]
..addAll(curr);
});
return x;
}
Here I get the error there should be at least a single stream to merge. Its because Observable.merge(streamList) is called before items are added to streamList.
I simply want to get the both results of two queries into one stream.
This should work.
//Change your streams here
Stream<List<QuerySnapshot>> getData() {
Stream stream1 = Firestore.instance.collection('list').where('id', isEqualTo: 'false').orderBy('timestamp').snapshots();
Stream stream2 = Firestore.instance.collection('list').where('id', isEqualTo: 'true').orderBy('timestamp').snapshots();
return StreamZip([stream1, stream2]);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: StreamBuilder(
stream: getData(),
builder: (BuildContext context, AsyncSnapshot<List<QuerySnapshot>> snapshot1) {
List<QuerySnapshot> querySnapshotData = snapshot1.data.toList();
//copy document snapshots from second stream to first so querySnapshotData[0].documents will have all documents from both query snapshots
querySnapshotData[0].documents.addAll(querySnapshotData[1].documents);
if (querySnapshotData[0].documents.isEmpty)
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: CircularProgressIndicator(),
)
],
);
if (querySnapshotData[0].documents.length == 0)
return const Center(
child: Text(
"Not Available",
style: TextStyle(fontSize: 30.0, color: Colors.grey),
),
);
return new ListView(
children: querySnapshotData[0].documents.map((DocumentSnapshot document){
// put your logic here. You will have access to document from both streams as "document" here
return new ListCard(document);
}).toList()
);
}
),
);
}
Hope this helps!!!
I’m not sure why you’re using forEach and Observable.just().
You can just merge two firestore streams directly like:
Observable.merge([stream1, stream2]).pipe(combineStream);
Wherre stream1/2 is just your firestore snapshot.
I used RxDart package to combine two streams as shown below
RxDart - CombineLatestStream
final Stream<DocumentSnapshot> user = Firestore.instance
.collection("users")
.document(firebaseUser.uid)
.snapshots();
final Stream<QuerySnapshot> cards =
Firestore.instance.collection("cards").snapshots();
CombineLatestStream.list([user, cards]).listen((data) {
add(LoadedHomeEvent(
data.elementAt(0),
data.elementAt(1),
));
});
Well I am late, but just gonna put it out there.
You can add whereIn clause in your query like this:
Firestore.instance.collection("collection_name").where("field",whereIn:["false","true"]).snapshots();
I was also trying to combine two streams from firestore (as querying does not support OR) and went about it like this:
import 'package:rxdart/rxdart.dart';
Rx.combineLatest2(
StreamQuerySnapshot1, //a snapshot from firestore
StreamQuerySnapshot2, //another snapshot from firestore
(var stream1, var stream2) {
return [...stream1.docs, ...stream2.docs]; //Concatenated list
}
)
This will emit changes no matter which streams is changing in contrast to other solutions I found which support emits only if both streams have changes.
The best way I found is to use MergeStream from RxDart
Stream<QuerySnapshot> searchResult() {
final falseRoomStream = FirebaseFirestore.instance
.collection('room-list')
.where('id', isEqualTo: 'false')
.snapshots();
final pendingRoomStream = FirebaseFirestore.instance
.collection('room-list')
.where('id', isEqualTo: 'pending')
.snapshots();
return MergeStream([falseRoomStream, pendingRoomStream]);
}

Resources