I want to user a StreamController to control a StreamBuilder that gets data from a collection in Firestore. This will enable me to use a RefereshIndicator so that when I pull down on the list, it refreshes/ fetches more data if there is any.
I used most of the information in this article. My current code is below
class _Lists extends State<List> {
StreamController _controller;
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
#override
void initState() {
_controller = new StreamController();
loadPosts();
super.initState();
}
Future fetchPost() async {
return await .
Firestore.instance.collection(_locationState).snapshots();
}
Future<Null> _handleRefresh() async {
count++;
print(count);
fetchPost().then((res) async {
_controller.add(res);
showSnack();
return null;
});
}
showSnack() {
return scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text('New content loaded'),
),
);
}
loadPosts() async {
fetchPost().then((res) async {
print(res.document);
_controller.add(res);
return res;
});
}
#override
Widget build(BuildContext context) {
final topBar = AppBar(Title("List"));
bottom: TabBar(
indicatorColor: Colors.blueAccent,
indicatorWeight: 3.0,
//indicatorSize: 2.0,
indicatorPadding:
const EdgeInsets.only(bottom: 10.0, left: 47.0, right:
47.0),
tabs: [
Tab(
child: Image(
image: AssetImage("MyImage1"),
width: 65.0,
height: 65.0,
),
),
Tab(
child: Image(
image: AssetImage("Image2"),
width: 90.0,
height: 90.0,
),
),
],
),
return DefaultTabController(
length: 2,
child: Scaffold(
key: scaffoldKey,
appBar: topBar,
body: StreamBuilder(
stream: _controller.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.connectionState == ConnectionState.active) {
List aList = new List();
aList.clear();
for (DocumentSnapshot _doc in snapshot.data.documents) {
Model _add = new Model.from(_doc);
aList.add(_add);
}
return TabBarView(
children: <Widget>[
RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView.builder(
itemCount: aList.length,
itemBuilder: (context, index) {
return Card(aList[index]);
},
),
),
Icon(Icons.directions_transit),
],
);
} else {
return Container(
child: Center(child: CircularProgressIndicator()));
}
})));
}
}
}
The Problem I have with this is I keep getting the error
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY
╞═══════════════════════════════════════════════════════════
flutter: The following NoSuchMethodError was thrown building
StreamBuilder<dynamic>(dirty, state:
flutter: _StreamBuilderBaseState<dynamic,
AsyncSnapshot<dynamic>>#53c04):
flutter: Class '_BroadcastStream<QuerySnapshot>' has no instance getter 'documents'.
flutter: Receiver: Instance of '_BroadcastStream<QuerySnapshot>'
flutter: Tried calling: documents
Any ideas on how to go about using the StreamController with data from Firestore ?
Keeping a close eye on return types in your IDE will likely help avoid a lot of confusing issues like this. Unfortunately, that blog does not indicate any types for the API call, StreamController, or 'res' in the then statement. Having those types declared will help show what you are working with (at least it does for me in Android Studio). For example in my StreamBuilder with a stream from Firestore, I use AsyncSnapshot<QuerySnapshot> snapshot instead of just AsyncSnapshot. This allows the tools in Android Studio to tell me that snapshot.data.documents is the map from the QuerySnapshot class. If I don't add the extra type, I can't see that.
Here is an example of listening to the stream from the Firestore Dart package.
//Performing a query:
Firestore.instance
.collection('talks')
.where("topic", isEqualTo: "flutter")
.snapshots()
.listen((data: QuerySnapshot) =>
// do stuff here
);
Since you're using the async/await style (also perfectly fine), you'll have the same result that will be inside of .listen((data) =>. We can follow the documentation/classes to see what types are returned.
Firestore.instance.collection(<whatever>).snapshots()
will return
Stream<QuerySnapshot>,
so we know that
await Firestore.instance.collection(<whatever>).snapshots()
will return
QuerySnapshot.
Digging further into the class, we see it has a property called documents.
/// Gets a list of all the documents included in this snapshot
final List<DocumentSnapshot> documents;
This finally gives us those DocumentSnapshots, which you'll have to pull the data property from.
So in your case, I believe the res type being QuerySnapshot will help show you what data to put in your stream, which can be done multiple ways at this point. List<DocumentSnapshot> seems to look like what you're going for, but you could go farther to List<YourClass> built from the DocumentSnapshot data property. With that, you can say what data type your StreamController will return, making the builder's AsyncSnapshot<your stream type> much more clear to work with.
I'm not sure what development tools you are using, but in case you aren't familiar most will allow you to do something like: press/hold (command or ctrl), hover over the type/class/function/var you want to see, left click, and you should be taken to the source files/declarations (I find this super handy).
Related
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.
I have a FutureBuilder widget that should wait for data from a firestore collection.
class MyScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: calendarQuery.getCalendarEntry(dateString),
builder: (BuildContext context, AsyncSnapshot snap) {
if (snap.hasData) {
List<Events> recipe = snap.data;
return Scaffold(
appBar: AppBar(
title: Text("Events"),
),
body: Column(
children: <Widget>[
...,
],
),
);
} else {
return LoadingScreen();
}
},
);
}
}
I retrieve a list of events and then for each event I need to fetch some additional details. I tried to do nested Futures and came up with the code below. It generates a Future<Iterable<Future<Detail>>> which ends up as MappedListIterable<DocumentSnapshot,Future<Recipe>> in snap.data and i cannot handle it.
class CalendarQuery<T> {
...
Future<Iterable<Future<Detail>>> getCalendarEntry(String date, String type) async {
return await ref
.where("date", isEqualTo: date)
.getDocuments()
.then((data) {
return data.documents.map((doc) => Document<Details>(path: 'detailCollection/${doc.data["Event"]["SomeId"]}')
.getData());
});
}
}
I think I went wrong here at some points with handling the futures and there is probably a proper way to do this.
Does someone know a way to refactor getCalendarEntry so that it returns a Future<List<T>>? Or maybe there is a better approach to solve this?
You can use Future.wait to create a Future that completes once all Futures from an Iterable have completed.
In your scenario, you would replace return await ref with return Future.wait(ref and a closing bracket where needed, to create a Future that waits for all Details to be retrieved.
I am trying to build an app with different lists of games. As a backend I use Firebase and the connection is working fine, I tested it. Anyway I have problems with replacing the mock data with real data from firebase. I always get this error:
type 'Future < dynamic>' is not a subtype of type 'List < Game>'
I have following function:
getGames() async{
List newGamesList = [];
QuerySnapshot result = awaitFirestore.instance.collection('products').getDocuments();
List<DocumentSnapshot> documents = result.documents;
documents.forEach((DocumentSnapshot doc) {
Game game = new Game.fromDocument(doc);
newGamesList.add(game);
});
}
"Game" looks like that:
factory Game.fromDocument(DocumentSnapshot document) {
return new Game(
name: document['name'],
box: document['box'],
cover: document['cover'],
description: document['description'],
);
}
In my build widget I call "getGames":
new HorizontalGameController(getGames()),
Any idea why this error occures and how to solve that?
EDIT:
For better understanding here is my HorizontalGameController:
class HorizontalGameController extends StatelessWidget {
HorizontalGameController(this.gameItems);
final List<Game> gameItems;
#override
Widget build(BuildContext context) {
return new SizedBox.fromSize(
size: const Size.fromHeight(240.0),
child: new ListView.builder(
itemCount: 1,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(left: 12.0, top: 4.0),
itemBuilder: (BuildContext context, int position) {
return GameContainerItem(context, gameItems[position]);
}),
);
}
}
getGames is not returning the gameList you created. Make the function return the list of games. I can't test it, but give this a try
Future<List<Game>> getGames() async{
List<Game> newGamesList = [];
QuerySnapshot result = await Firestore.instance.collection('products').getDocuments();
List<DocumentSnapshot> documents = result.documents;
documents.forEach((DocumentSnapshot doc) {
Game game = new Game.fromDocument(doc);
newGamesList.add(game);
});
return newGamesList;
}
//then this
//new HorizontalGameController(await getGames()) //change this
EDIT
Change new HorizontalGameController(await getGames()) to the code below (wrap it with a futureBuilder). This will enable the widget make use of the future value.
FutureBuilder<List<Game>>(
future: getGames(),
builder: (context, AsyncSnapshot<List<Game>> gamelistSnapshot){
return (gamelistSnapshot.hasData)? HorizontalGameController(gamelistSnapshot.data) : Container();
},
)
This happens in my main application
and
I replicate it with the given codelabs:
https://codelabs.developers.google.com/codelabs/flutter-firebase/index.html?index=..%2F..index#10
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Baby Names',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Baby Name Votes')),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('baby').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot){
return ListView(
padding: const EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
return Padding(
key: ValueKey(record.name),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5.0),
),
child: ListTile(
title: Text(record.name),
trailing: Text(record.votes.toString()),
onTap: () => print(record),
),
),
);
}
}
class Record {
final String name;
final int votes;
final DocumentReference reference;
Record.fromMap(Map<String, dynamic> map, {this.reference})
: assert(map['name'] != null),
assert(map['votes'] != null),
name = map['name'],
votes = map['votes'];
Record.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(snapshot.data, reference: snapshot.reference);
#override
String toString() => "Record<$name:$votes>";
}
Only plugin i use is the cloud_firestore 0.9.5+2.You need to be patient with this testing process please. You will not see the issue right away. Run the app first, set this project up. You can follow the directions in the given codelabs. Once everything is set up on front end and backend(create documents on firstore). Go have a lunch break, dinner, play video games or hang out with friends. Come back after 1 hour. Run the app, you will be incurred charges for those reads as new. Do it again, come back 1 hour and it will happen again
How to replicate it in real life:
Start this app by given code from codelabs. Run it, it should incur 4 document reads if you stored 4 documents into the firestore.
Start it up again. No reads are charged. Great it works! but no, it really doesnt.
I wake up next day and open up the app, im charged 4 reads. Okay maybe some magic happened. I restart it right away and no charges incur(great!). Later in 1 hour, i start up the app and I get charged 4 reads to display the very same 4 documents that have not been changed at all.
The problem is, on app start up. It seems to be downloading documents from the query snapshot. No changes have been made to the documents. This stream-builder has been previously run many times.
Offline mode(airplane mode), the cached data is displayed with no issues.
in my main application for example, I have a photoUrl and on fresh App start, you can see it being loaded from the firestore(meaning downloaded as a fresh document thus incurring a READ charge). I restart my main application, no charges are made and photo does not refresh(great!). 1 hour later i start up the app and charges are made for every document I retrieve(none of changed).
Is this how cloud firestore is supposed to behave?
From what I have read, its not supposed to behave like this :(
You should not do actual work in build() except building the widget tree
Instead of code like this in build
stream: Firestore.instance.collection('baby').snapshots(),
you should use
Stream<Snapshot> babyStream;
#override
void initState() {
super.initState();
babyStream = Firestore.instance.collection('baby').snapshots();
}
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: babyStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
The FutureBuilder docs don't mention it that explicitly but its the same
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateConfig, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.
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();
},
),