Build Error in Dart / Flutter during build - firebase

So in my build function, I load my user data, and once it is loaded I would like to change the theme of the app (which calls setState()). The issue is that I can't call setState during the build process, like the error below states. How would I go about loading a user selected theme on app startup? Also, is it not recommended to be loading data inside the build function? It seems to work well but feels kinda gross. Thanks!
Widget build(BuildContext context) {
//get user object from Firebase
//once user is loaded, take their chosen color theme and call updateTheme()
}
Error:
This ThemeSwitcherWidget widget cannot be marked as needing to build because the framework is
I/flutter (23889): already in the process of building widgets. A widget can be marked as needing to be built during the
I/flutter (23889): build phase only if one of its ancestors is currently building. This exception is allowed because
I/flutter (23889): the framework builds parent widgets before children, which means a dirty descendant will always be

For loading data or doing a blocking call, you should use FutureBuilder or StreamBuilder (I am not much aware of firebase APIs so can't tell which one to use, but both of them are very similar.) It takes a future or stream as an argument and builds based on it. I am assuming you know about future API of dart
Here is some example code that will give you some idea.
StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new SplashScreen();
} else {
if (snapshot.hasData) {
return new MainScreen(firestore: firestore, uuid: snapshot.data.uid);
}
return new LoginScreen();
}
}
)

Related

Flutter Firebase Auth + Firestore Provider user document

A common scenario in nearly every single Flutter Firebase project is to use authStateChanges to determine logged in/out state by checking if the User object is null. But generally there are extra fields that are needed on a user, which come from Firestore. (I usually make a FirestoreUser model). It's generally really nice to be able to stream the current user's Firestore document and have it available throughout the app. But there's a few issues.
final user = context.read<User?>();
return StreamProvider<FirestoreUser?>(
create: (_) => UserRepository(FirebaseFirestore.instance)
.streamUser(user!.uid),
initialData: null,
child: const HomeScreen(),
);
StreamProvider's require an initialData. The end goal would be to use context.watch<FirestoreUser>() throughout the app, but since User (FirebaseAuth user) can be null, FirestoreUser (Firestore user) also has to be null.
final user = context.watch<FirestoreUser?>();
// this should technically never happen, but still need to check for it because it's impossible(?) to Provide a non-null FirestoreUser
if(user == null) return SizedBox.shrink();
...
return Text('Hello ${user.displayName}');
It would be super annoying if I had to do this on every. single. page. (This is going to be a large application)
The FirestoreUser should be available throughout the app, so it needs to be above MaterialApp. This makes the logged in/out logic complicated, since your login screen needs to be below MaterialApp.
I am also using MaterialApp.router using the Beamer package. They have a bunch of examples, but none of them are actually useful. Their FirebaseAuth example has everything in one widget and would never be a real world scenario.
Also, it's been a good while since null-safety was introduced but there are so few examples/tutorials out there that actually implement null-safety.
I'm convinced it is impossible to listen to authStateChanges to control logged in/out state and then only providing a non-null Firestore document to use throughout the application using MaterialApp.router. Is that asking too much? I feel like this would be the most common scenario. It's one I've been struggling with for my past 5+ apps.
I implemented the same scenario with MaterialApp.router, I hope my solution helps you.
I set up a ChangeNotifierProvider to handle authentication state called MyAuthState. MyAuthState is placed above MaterialApp.router, I actually use more providers, so it looks like the following. For the navigation to be accessibly (e.g. route the user to login etc.) I add the RouterDelegate to the ChangeNotifierProvider:
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => MyAuthState(
routerDelegate: ...)),
...
],
builder: (context, child) {
return MaterialApp.router(
...
);
}
Then in MyAuthState I listen to Firebase Authentication state changes, save current authentication state and start the stream to listen to my own Firestore user collection if a user is authenticated. Something like:
StreamSubscription<MyUser?>? _myUserStream;
...
FirebaseAuth.instance.authStateChanges().listen((User? firebaseUser) {
// save authentication state to know whether a user is authenticated
// if it is not null, start a stream
_myUserStream = <stream to listen snapshots for my own user collection>;
notifyListeners();
});
After this I use an authentication router as the home route of my application, consuming MyAuthState provider, and this will decide whether to show a login screen or something else (the code uses an enum for the different states):
class _MyAuthRouter extends State<MyAuthRouter> {
#override
Widget build(BuildContext context) {
return Consumer<MyAuthState>(builder: (context, myAuthState, child) {
switch (myAuthState.status) {
// before first auth state => progress indicator
case MyAuthStatus.unknown:
return const CircularProgressIndicator();
// user is logged out, not found or disabled => login screen
case MyAuthStatus.loggedOut:
case MyAuthStatus.notFound:
case MyAuthStatus.disabled:
return const MyAuthLogin();
// email not verified => verify email screen
case MyAuthStatus.emailUnverified:
return const MyAuthVerifyEmail();
// logged in => home screen
case MyAuthStatus.loggedIn:
return const MyPageHome();
}
});
}
}
And since I only get to MyPageHome upon user is logged in, I know that the stream for my user collection will be available and I can use context.read or context.watch to get the user's data.

Retrieving firebase firestore documents in Flutter / Dart and Sound Null

I am working my way through a Udemy Flutter class and I am in a chapter dealing with Firebase. The class is about 3-4 years old and it seems is just old enough that the sample completed code crashes when accessing the Firebase portions. I started a new project from scratch and cobbled bits and pieces to get it mostly up and running but I have now hit a dead end. The new project and the firebase plugins are Sound Null and the class code is not. Getting and printing data from the database worked fine until I tried getting it hooked up to a stream. I am specifically running into a problem iterating over the received documents.
When I set up the message variable using (the Flutter) snapshot.data in a for-in loop and try to iterate over the returned documents (#1) I have a null problem. Without specifying the type (#1a) as AsyncSnapshot<dynamic> the for-in loop errors that I cannot iterate over a non nullable.
Changing the type gets rid of the compile time error but generates a runtime error of
Type _JsonQuerySnapshot is not a subtype of type Iterable
Dart is not my primary language and I have been pulling my hair out googling this for several hours to no avail. Any help is appreciated.
final _firestore = FirebaseFirestore.instance; //<-----earlier in the code
children: <Widget>[
StreamBuilder<QuerySnapshot>(
stream: _firestore.collection('messages').snapshots(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) { //<----Problem #1a here
List<Text> messageWidgets = [];
if (snapshot.hasData) {
final messages = snapshot.data;
for (var message in messages) { //<----------Problem #1 here
final messageText = message.data['text'];
final messageSender = message.data['sender'];
final messageWidget =
Text('$messageText from $messageSender');
messageWidgets.add(messageWidget);
}
}
return Column(
children: messageWidgets,
);
},
Well, after another hour or so of googling around I found the right place to look. The docs for the flutter plugin for firestore have a massive difference from the course I am taking. Reading(ctrl-c, ctrl-v) them I'm at least able to mostly understand and have the program back on track to finish my course.

snapshot returns always empty data in StreamBuilder flutter

So, I'm facing this problem: the 'snapshot' doesn't get any data from Firestore in StreamBuilder in Flutter.$
Here is the code:
StreamBuilder<Driver>(
initialData: null,
stream: DatabaseServices(uid: driver.uid).driverData,
builder: (streamContext, snapshot) {
print(driver.uid);
if (snapshot.hasData) {
Driver currentDriver = snapshot.data;
print(currentDriver.fullName);
print(currentDriver.email);
} else {
print('no data');
}
}
)
Note: stream: DatabaseServices(uid: driver.uid).driverData
-> driver here works fine on top of the whole code and gets the driver data such as uid.
And this code always returns 'no data'.
The weird thing here is that I'm using the same code (with another kind of user -> Client) in another screen, and it works normally, and it gets the data properly.
And in Firestore, I have 2 collections, Driver and Clients, almost the same attributes.
It even has a SubCollection for both collections and it called 'Notification', and I'm using a StreamBuilder to show the notifications for both Client and Driver and it works normally.
Problem solved, the problem was in some attribute that I called it with the wrong name that used in the other collection (Client), I forgot to change it.

Provider with loading widget in Flutter [duplicate]

This question already has an answer here:
How to create a StreamProvider and subscribe to it later, Flutter
(1 answer)
Closed 1 year ago.
I have a stream provider in my Flutter app which fetches data from my Firestore database.
I want to show a loading widget while loading the data until it is done loading. Similar to the Futurebuilder with the ConnectionState function for example.
Is this possible?
Thank you for help!
When you use a future builder you can check if your snapshot.data is null. If it is null you show the spinner, otherwise you show your content:
StreamBuilder(
stream: myStream, //YOUR STREAM
builder: (BuildContext context, AsyncSnapshot snapshot) {
return snapshot.data == null
? Spinner() //LOADING SPINNER
: Container() //AFTER RETRIEVING CONTENT
}
)
UPDATE
Since you mentioned Future builder I got mislead. This is the answer you are searching for:
How to create a StreamProvider and subscribe to it later, Flutter

FutureBuilder doesn't work without touching the screen

I have the following situation: there's a button on the screen, which adds data to db. And there's the future builder which has future getDataFromDB. When I add data by button, the future builder doesn't get data from DB. And when I do several swipes on the screen it works correctly. What's the matter? Here's the code:
FutureBuilder(
future: DatabaseManager().findAllCaloriesForSelectedDate(currentDate),
builder: (context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
_caloriesCurrent = snapshot.data;
return AnimatedCircularChart(
size: Size(constraints.maxWidth * 0.8, constraints.maxWidth * 0.8),
initialChartData: <CircularStackEntry>[
CircularStackEntry(
<CircularSegmentEntry>[
CircularSegmentEntry(
currentProgress,
Color(AppColors.brandViolet),
),
CircularSegmentEntry(
100 - currentProgress,
Color(AppColors.layoutBackgroundColor),
),
],
),
],
chartType: CircularChartType.Radial,
edgeStyle: SegmentEdgeStyle.round,
percentageValues: true,
);
} else {
return Container();
}
},
)
I'll appreciate any help. Thanks in advance!
There're two problems with your code:
You're obtaining Future object within build function which isn't right way to do it. Every time your widget rebuilds, you get new Future object (so, maybe, it rebuilds when you touch/scroll). You should store Future object in State of your Widget.
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.
You're using FutureBuilder, but you expect FutureBuilder to rebuild after you change some data. FutureBuilder doesn't work like that and rebuilds only once - when Future value is resolved (if you store Future object within State. In your case, it always gets new Future on rebuild). You probably want to use StreamBuilder. It will allow you to add new data to the stream of data. StreamBuilder will trigger on stream changes and automatically rebuild.
You should read about BloC Architecture, in case you haven't.

Resources