Flutter using StreamBuilder with setState updates - firebase

I'm using a StreamBuilder inside a build method to retrieve user info from firestore and right after it is displayed with a Listview.builder.
Once i have retrieved the user info, i call another API with the user id's to retrieve some other info i would like to display in the list. This gives me a Future for each user, once the future has been "fulfilled" (inside Future.then()), then i save the data and call SetState(); to rebuild the list with the new data.
The problem is once setState() is called the Build method is rerun which gives me the users again, and this causes an endless loop of retrieving the users and the data from the second API.
This seems like a typical scenario but i don't know how to solve this with StreamBuilder. My previous solution retrieved the users in initState so it was not recalled endlessly.
Any tips?
"getOtherAPIData" loops the users retrieved and adds data "distance" to the list, and the list is sorted based on the user with the most/least distance.
Code:
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<List<IsFriend>>(
stream: viewModel.isFriendStream(),
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
List<IsFriend> friends = snapshot.data;
if (friends == null || friends.isEmpty) {
return Scaffold(
body: Center(
child: Text("Friend list is empty :("),
)
);
} else {
userList = friends;
getOtherAPIData();
return getListview(userList);
}
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
)
);
}
),
..
..
void getOtherAPIData() async {
for (IsFriend friend in userList) {
await fitbitServ.getData(friend.user.uid).then((fitData) {
setState(() {
setDistanceOnUser(fitData.distanceKm, friend.user.uid);
totalDistance += fitData.distanceKm;
sortUsers(userDataList);
userData[friend.user.uid] = fitData;
});
});
}
}

You should never call a method that starts loading data from within your build method. The normal flow I use is:
Start loading the data in the constructor of the widget.
Call setState when the data is available.
Render the data from the state in build.

Related

Flutter Firestore: FirestoreBuilder with initial data

I'm making my first Flutter app and I encounter a problem and doesn't found any solution for it.
I have a view where I render a Firestore document, and there is two ways of getting there:
From a list where I already loaded my documents
From Dynamic Links with uid attached as arguments (args)
So in order to listen document changes and loading the data when arriving from the link I used FirestoreBuilder like this:
return FirestoreBuilder<EventDocumentSnapshot>(
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<EventDocumentSnapshot> snapshot, Widget? child) {
if (!snapshot.hasData) {
return Container();
}
Event? event = snapshot.requireData.data;
return Scafold(); //Rest of my rendering code
}
);
How I could avoid first call to Firebase when I already have the data but still listen to changes? The main problem is that my hero animation doesn't work because of this.
I tried with a StreamBuilder and initialDataparam but since it's expecting stream I didn't know how to cast my data.
Okay, so I found the solution myself after many tries, so I added my Model object that can be null as initialData, but the thing that makes me struggle with is how you get the data in the builder. You have to call different methods depending on where the data is coming from.
return StreamBuilder(
initialData: args.event
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
// Here is the trick, when data is coming from initialData you only
// need to call requireData to get your Model
Event event = snapshot.requireData is EventDocumentSnapshot ? snapshot.requireData.data : snapshot.requireData;
return Scafold(); //Rest of my rendering code
}
);
Reading through cloud_firestore's documentation you can see that a Stream from a Query can be obtained via snapshots()
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text(document['author']),
);
}).toList(),
);
},
);
This won't help you, but with GetX it's simple to implement like this: You don't need StreamBuilder anymore.
//GetXcontroller
class pageController extends GetXcontroller {
...
RxList<EventModel> events = RxList<EventModel>([]);
Stream<List<EventModel>> eventStream(User? firebaseUser) =>
FirebaseFirestore.instance
.collection('events')
.snapshots()
.map((query) =>
query.docs.map((item) => UserModel.fromMap(item)).toList());
#override
void onReady() async {
super.onReady();
events.bindStream(
eventStream(controller.firebaseUser)); // subscribe any change of events collection
}
#override
onClose() {
super.onClose();
events.close(); //close rxObject to stop stream
}
...
}
You can use document snapshots on StreamBuilder.stream. You might want to abstract the call to firebase and map it to an entity you defined.
MyEntity fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snap) {
final data = snap.data()!;
return MyEntity (
id: snap.id,
name: data['name'],
);
}
Stream<MyEntity> streamEntity(String uid) {
return firebaseCollection
.doc(uid)
.snapshots()
.map((snapshot) => fromSnapshot(snapshot));
}
return StreamBuilder<MyEntity>(
// you can use firebaseCollection.doc(uid).snapshots() directly
stream: streamEntity(args.uid),
builder: (context, snapshot) {
if (snapshot.hasData) {
// do something with snapshot.data
return Scaffold(...);
} else {
// e.g. return progress indicator if there is no data
return Center(child: CircularProgressIndicator());
}
},
);
For more complex data models you might want to look at simple state management or patterns such as BLoC.

How to display a Firebase list in REAL TIME?

I have a TODO List function (Alarmas), but I feel I'm not taking advantage of Firebase's Realtime features enough.
The Widget displays the list very well, however when someone puts a new task from another cell phone, I am not being able to show it automatically, but I must call the build again by clicking on the "TODO button" in the BottomNavigationBar.
Is there a way that the new tasks are automatically displayed without doing anything?
I'm using streams to get the list...
#override
Widget build(BuildContext context) {
alarmaBloc.cargarAlarmas();
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
if (tareasList.length == 0) return _imagenInicial(context);
return ListView(
children: [
for (var itemPendiente in tareasList)
_crearItem(context, alarmaBloc, itemPendiente),
//more widgets
],
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('Preloader.gif'), height: 200.0,));
},
),
And, I read the Firebase Data in this way...
Future<List<AlarmaModel>> cargarAlarmas() async {
final List<AlarmaModel> alarmaList = new List();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp); // element.snapshot.value.
});
await resp.once().then((snapshot) {
print("Total list was loaded - ${alarmaList.length}");
}); //I'm using this await to be sure that the full list was loaded, so I can order and process it later
return alarmaList;
}
How can I display a List from Firebase in "true" Real Time?
To properly manage the state of asynchronously loaded data, you should:
Start loading/listening to the data in initState()
Set the data into the state (with setState()) when you receive it or it is updated.
Then render it from the state in the build method.
So in your code that'd be something like:
final List<AlarmaModel> alarmaList = new List(); // this is now a field in the `State` object
#override
void initState() {
super.initState();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp);
setState(() {
alarmaList = alarmaList;
})
});
}
#override
Widget build(BuildContext context) {
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
If you only want to repaint once you've gotten a complete update from the database, you can put the call to setState() in a value listener, just use onValue in that instead of once(), as you also want to catch the updates.

Flutter with firebase trying to access the current user on a different screen or widget than my login screen

So I have a dart file for my login screen and then a different home file. I'm trying to access the user id in the home file to get the users unique Cloud Firestore document. However whenever I call getUser() it says Future is not a subtype of type 'String'. Does that mean that for some reason the Future isn't being unwrapped to return the uid String?
getUser() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
return user.uid;
}
Here is where I call get user in a different dart file than the authentication but in the same file as my getUser() function.
return StreamBuilder<DocumentSnapshot>(
stream: Firestore.instance.collection('watchlists').document(getUser()).snapshots(),
builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot){
if (snapshot.hasError) {
return new Center(
child: Text('Error: ${snapshot.error}'),
);
}
if (!snapshot.hasData) {
return new Center(
child: CircularProgressIndicator(),
);
}
else {
var documents = snapshot.data.data;
var vals = documents.values;
List<WatchListItem> watchListItems = [];
for (int x = 0; x < vals.length; x++) {
watchListItems.add(
WatchListItem(
symbol: vals.elementAt(x).toString(),
price: 190.79,
),
);
}
return new Center(
child: ListView(
children: watchListItems,
),
);
}
},
);
When I try getUser().toString() the error I get is that the getter 'values' was called on null (I am trying to get database values). Can the current user only be accessed at the time of authentication? Or can it be accessed anywhere in the app at any time as long as a user is logged in? A workaround I see is to get the uid at login and then pass it as a parameter to every new Widget I go to. I'm having a really tough time with this because somehow Google barely has any documentation on using their own Flutter with their Firebase.
getUser() returns a Future<String> while document() accepts a String as an argument:
/// so that the resulting list will be chronologically-sorted.
DocumentReference document([String path]) =>
DocumentReference._(_delegate.document(path), firestore);
Therefore the best place to call getUser() is inside initState. Inside initState you can do:
String userId = await getUser();
Then use the userId in document()

future.then block skipped inside function of return type integer causes to return null

I have stumbled upon a weird problem.
The following function gets a Firestore document and returns it so other functions can access it's data
Future getCountRequests() async {
try{
return await _countReference.document('Requests').get();
} catch(e){
print(e.toString());
}
}
And this is the function in question that uses it's data.
int _countRequest() {
int toReturn;
CounterService().getCountRequests().then(
(doc) {
//print('Item in question: $doc, information from document ${doc.data}');
//this line prints correctly Instance of DocumentReference and {"amount" : 6}
toReturn = doc.data['amount'];
}
);
return toReturn;
}
When I run the code, I get an error message on my screen which states that the AnimatedList I am using receives null from the _countRequest() function.
Putting a break on this line has helped me understand that this block gets skipped completely
CounterService().getCountRequests().then( ...
However when I put a break on this line, it shows that the code inside the block works and that the document is indeed received through the getCountRequests() function.
toReturn = doc.data['amount'];
My question is, what causes the .then block to be skipped causing the function to return null?
What you are trying to do cannot work.
You are trying to return the result of an asynchronous computation from a synchronous function. There is no way to make that work.
An asynchronous computation will always complete later, and a synchronous function always returns now, so the result of the computation cannot be returned now.
The then call is what makes the operation asynchronous. It passes a callback to a future, but the only thing you are guaranteed about when that callback is called is that it is definitely not immediately. The then call returns immediately, returning a future which will be completed when the callback has been called, and your code returns the current value of toReturn, which is still null.
In the end I figured that doing this asynchronously with a FutureBuilder wouldn't work at all, or with my current skillset I just can't figure out how to make it work. So I decided to convert to a StreamBuilder which uses a Stream<QuerySnapshot> gotten from the following function.
Stream<QuerySnapshot> findAllRequests() {
try{
return accountRequestCollection.snapshots();
} catch(e){
print(e.toString());
return null;
}
}
and built the AnimatedList into the StreamBuilder like so.
class Request extends StatefulWidget {
#override
_RequestState createState() => _RequestState();
}
class _RequestState extends State<Request> {
final AdministrationService _administrationService = AdministrationService();
final GlobalKey<AnimatedListState> _globalKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Account Requests'),
),
body: StreamBuilder(
stream: _administrationService.findAllRequests(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Loading();
default: if (snapshot.hasError) {
return new Text('Error: ${snapshot.hasError}');
} else {
return AnimatedList(
key: _globalKey,
initialItemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index, Animation animation) {
return _buildItem(snapshot.data.documents[index].data['email'], animation);
}
);
}
}
}
)
);
}
Widget _buildItem(String item, Animation animation) {
return SizeTransition(
sizeFactor: animation,
child: Card(
child: ListTile(
title: Text(
item,
style: TextStyle(
fontSize: 20.0,
)
),
),
),
);
}
}
It results into this page.
Emulator Screenshot.
I hope that this solution can help others with similar problems, though I am not sure how costly this could get when it comes to firestore read/writes considering this is just a proof of concept product.

Getting and storing user data after logging/sign up

I want to load user profile information from firestore after the user logs in and I want that data to be available to the whole app when the data is logged in. I plan on doing the later part using inherited widgets in flutter. I can load the data from firestore correctly and get all the data but the home page does not wait for the data to load before continuing so I get errors. It calls the method, then ignores the await part in the method and proceeds on, so I get errors, then after the errors, I get the data messages that the data has loaded.
I have tried a couple of things. I have all my user management code (login, sign up, ...) in one file and I wrote a getuserProfie() method in that file. The method gets all the data, populates a user model with the data and returns that model. See code below.
getUserProfile() async {
UserModel theUser;
await FirebaseAuth.instance.currentUser().then((user) {
Firestore.instance
.collection('/users')
.where('uid', isEqualTo: user.uid)
.snapshots()
.listen((data) {
print('getting data');
theUser = new UserModel(user.uid, data.documents[0]['email'],);
print('got data');
print(theUser.email);
return theUser;
});
}).catchError((e) {
print("there was an error");
print(e);
});
}
I then call this method in my home page as follows
#override
void initState() {
Usermode user = UserManagement().getUserProfile(); //Calling the method
super.initState();
}
This calls the method just fine and gets all the data but the program does not wait until the data is gotten to proceed. I tried moving the code that gets the data into the initState() method and calling super.initState() when I was sure there was a value but that did not work. I also tried calling the getUserProfile() before calling super.initstate() but that did not work.
I tried adding async to the initState() header but flutter does not like that. I can get the data from firebase fine but making the program wait until the data is gotten is the problem I am having. Seems like using await and async is not working. Any other suggestions to make sure the data is loaded before continuing ? I thought about using FutureBuilder but I am not sure how that would work in this case.
initState() can't be a async function.
What You Can try is - Move Usermode user = await UserManagement().getUserProfile(); to a new async function.
Usermode user;
#override
void initState() {
super.initState();
_getUser();
}
Void _getUser() async {
user = await UserManagement().getUserProfile();
setState(() {});
}
and in Build Method you can Check for User Value then pass the Widgets.
#override
Widget build(BuildContext context) {
if (user != null) {
return ProfilePage();
} else {
return CircularProgressIndicator();
}
Now whenever User Data is Loaded it will call setState() & your Profile Page will Load.
For this you need to make - Usermode user; state variable.
Since getUserProfile is async, you'll need to use await when calling it:
#override
void initState() async {
Usermode user = await UserManagement().getUserProfile();
super.initState();
}
After doing much research, I ended up using Flutter StreamBuilder. It was perfect for what I wanted to use it for. I call the method UserManagement().getUserProfile() and get the snapshot of the data from the returned value. If the data is loading, I display the CircularProgressIndicator(). If the snapshot is null, I display error message. See code snippet below.
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: UserManagement().getClientProfile(curUserID).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
UserModel _user = new UserModel.from(snapshot.data)
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(_usermodel.name),
],
});
}else if (snapshot.connectionState == ConnectionState.waiting) {
return Container(child: Center(child: CircularProgressIndicator()));
} else {
return Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.warning),
),
Text('There was an error please try again')
],
),
);
}
}
In this example, I am using StreamBuilder but you can also use FutureBuilder. StreamBuilder listens for changes in the data and updates the UI accordingly while FutureBuilder gets the data once until the page is loaded again. I had to make changes to my UserModel and also adjusted the way I stored my data but it all worked out. Got most of my solution from here

Resources