My goal is to have a userNotifications collection in Firestore that is used to track user notification preferences. The user can toggle between true and false based on tapping a switch tile. With the code below, Firestore is immediately and correctly updating the boolean value based on user interaction and the user interface is updating and reflecting the changes. If the user logs out and logs back in, the boolean value is accurately reflected in the user interface.
Before I apply this approach more broadly in the code I was hoping that someone can comment and let me know if my approach to updating the boolean value in Firestore is valid or point me in a better direction so I can improve my code. Links to SO posts or documentation are fine as I am more than willing to read and learn. Thanks in advance for any help.
class NotificationsMessagesTile extends StatefulWidget {
const NotificationsMessagesTile({
Key? key,
}) : super(key: key);
#override
State<NotificationsMessagesTile> createState() =>
_NotificationsMessagesTileState();
}
class _NotificationsMessagesTileState extends State<NotificationsMessagesTile> {
bool notificationsActive = false;
final String? currentSignedInUserID = Auth().currentUser?.uid;
Future<void> updateNotifications() async {
if (!notificationsActive) {
notificationsActive = true;
FirebaseFirestore.instance
.collection('userNotifications')
.doc(currentSignedInUserID)
.update({
'messages': false,
});
} else {
notificationsActive = false;
FirebaseFirestore.instance
.collection('userNotifications')
.doc(currentSignedInUserID)
.update({
'messages': true,
});
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return SwitchListTileSliver(
icon: Provider.of<NotificationsPageProvider>(context).areMessagesTurnedOn
? Icons.notifications_active
: Icons.notifications_off,
onChanged: (bool value) {
final provider = Provider.of<NotificationsPageProvider>(
context,
listen: false,
);
provider.updateMessagesSettings(isOn: value);
updateNotifications();
},
subTitle:
Provider.of<NotificationsPageProvider>(context).areMessagesTurnedOn
? const Text(
SettingsPageString.messagesOn,
)
: const Text(
SettingsPageString.messagesOff,
),
title: SettingsPageString.messages,
value:
Provider.of<NotificationsPageProvider>(context).areMessagesTurnedOn,
);
}
}
You could improve your updateNotifications() function to not have duplicate code:
Future<void> updateNotifications() async {
await FirebaseFirestore.instance
.collection('userNotifications')
.doc(currentSignedInUserID)
.update({
'messages': notificationsActive,
});
setState(() {
notificationsActive = !notificationsActive;
});
}
And also I would suggest to you to listen to your Firestore collection and update UI on change. You can look up on how to do that here.
Related
In my app, I have the following providers.
#override
Widget build(BuildContext context) {
return OverlaySupport.global(
child: MultiProvider(
providers: [userLoggedIn, currentUserData],
child: MaterialApp(...)))
}
var userLoggedIn = StreamProvider<User?>.value(
value: FirebaseAuth.instance.authStateChanges(), initialData: null);
var currentUserData = StreamProvider<FrediUser>.value(
updateShouldNotify: (_, __) => true,
initialData: FrediUser(
loginProvider: '',
email: '',
admin: false,
profileSettings: [],
profileChips: [],
profileStats: [],
id: 'loading',
imageUrl: 'loading',
bio: 'loading',
username: 'loading'),
value: currentUserID != null ? currentUserDataStream() : null,
);
PROBLEM
When the user logs out (or logs in for the first time), the provider is either:
Containing old user data (until a hot restart is done, when the providers are called again and reloaded)
Null or empty, because there was no user before.
What I want to do is to refresh or call the Stream Providers again once I have a new user, or delete all the data once a user logs off.
Thank you!
You can listen to the changes of auth state like this.
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
I've been facing a similar problem as you are, I've come up with a work-around although not sure how "valid" it is according to the Provider architecture
The Problem
I've got a DatabaseService class which has a stream function of type Stream<CustomUser> function, and I used it like this:
//--- main.dart ---//
runApp(MultiProvider(
providers: [
// ..some other providers.. //
// data provider
Provider<DatabaseService?>(
create: (_) => databaseService,
),
// data provider
StreamProvider<CustomUser?>(
create: (context) => databaseService.getCurrUserFromDb(),
lazy: false,
initialData: null,
updateShouldNotify: (_, __) => true,
),
],
child: MyApp(
initPage: initPage,
)
));
Stream Function:
//--- database_service.dart ---//
// gets the user from database and
// assigns it to the variable _user.
Stream<CustomUser?> getCurrUserFromDB() async* {
try {
CustomUser? currUser;
if (_user != null) {
await for (DocumentSnapshot<Object?> event
in users.doc(user.uid).snapshots()) {
final jsonMap = event.data() as Map<String, dynamic>;
currUser = CustomUser.fromJson(jsonMap);
_user = currUser;
CustomPreferences.setCurrUser(_user);
yield currUser;
}
}
} catch (e) {
rethrow;
}
}
databaseService is the DatabaseService class with named constructors.
This was not causing the widgets to rebuild at the start nor when the stream has a new value
Solution:
Created a StreamController in the DatabaseService class, and when the user signs in I add the stream function:getCurrUserFromDB() to the StreamController like this
//--- authentication_screen.dart ---//
...
ElevatedButton(
child: const Text("Sign In"),
onPressed: () async {
final user = await AuthService().googleSignIn();
if (user != null) {
final dbProvider = context.read<DatabaseService?>();
await dbProvider?.setInitUser(user, context);
await dbProvider?.cusUserController
.addStream(dbProvider.getCurrUserFromDB());
}
}),
...
setInitUser(CustomUser? user) is used set the value of the _user variable in DatabaseService and user is used to get this variable.
Reasoning
I am creating a StreamProvider at the start of the app, and its source the StreamController needs to have a stream to listen so I give it when I am trying to sign in.
Or even cleaner solution would be to do it in the constructor of DatabaseService Class like this:
//--- database_service.dart ---//
// method to add the stream to controller //
Future<void> addStream() async {
if (!_cusUserController.isClosed && !_cusUserController.isPaused) {
await _cusUserController.addStream(getCurrUserFromDB());
}
}
// constructor //
DatabaseService._init(CustomUser cusUser) {
addStream();
_user = cusUser;
}
And one last thing to note is that I don't make the declare the Controller as final. When I had it declared as final the streams weren't updating, so it looks like this now:
//--- database_service.dart ---//
StreamController<CustomUser?> _cusUserController = StreamController();
TL;DR
I created a StreamProvider which returns a StreamController in its create property and later down the life cycle of the app I gave the controller a Stream using the addStream method.
Sorry for the wall of text I just wanted to come out as clear as possible.
I have a very tricky situation, which I've reproduced in a demo.
I have a Provider of a user, with this method of updating the listeners:
class User extends ChangeNotifier {
...
User({required this.uid}) {
Database().getUser(uid).listen(
(user) async {
displayName = user?.displayName;
email = user?.email;
phoneNumber = user?.phoneNumber;
photoURL = user?.photoURL;
did = user?.did;
interests = user?.interests;
notifyListeners();
},
onError: (e) => print(e),
);
}
...
}
My main.dart starts like this:
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthState>.value(value: _authState),
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel())
],
child: Consumer<AuthState>(
builder: (context, auth, child) {
var user =
auth.authUser == null ? null : User(uid: auth.authUser!.uid);
return MultiProvider(
providers: [
ChangeNotifierProvider<ZUser?>.value(
value: zuser,
),
],
child: MaterialApp.router(...
This has been sufficient for my use case thus far.
Now, I wish to make an update to the interests field;
I have a DB widget that does:
Future updateUser(String uid, Map<String, Object?> data) async {
return userCollection.doc(uid).update(data);
}
Where the userCollection is my collection in Firestore.
I call this class from my view widget, as:
ZWideButton(
text: "Save",
onPressed: () async {
setState(() {
_localEdit = false;
_loading = true;
});
await user.saveInterests(_interests());
setState(() => _loading = false);
},
),
Where saveInterests is:
Future saveInterests(List<String> interests) async {
return _db.updateUser(uid, {"interests": interests});
}
None of this presents any problem at first -- I can update the interests and it works fine. That is, until I keep updating the interests, and it gets slower and slower each time (the browser says the download time gets longer and longer) and seemingly my computer is eating up more and more memory until the webpage ultimately crashes.
Something of a memory leak appears to be happening, but I'm unsure what about flutter web and firebase could be causing it. I believe it may have to do with the Provider package not disposing appropriately. It does not seem to be the provider as I don't see the Widget being rebuilt over and over. Looking for some thoughts.
For anyone looking; My issue is that my json deserializer was causing an infinite loop with the firebase listener
I am working on an app where I need to filter out every user that signs in. Once they signed in, they will be redirected to wrapper that checks if the user ID exists on Firestore document collection. Here is my code.
class adminCheck extends StatefulWidget {
const adminCheck({Key? key}) : super(key: key);
#override
State<adminCheck> createState() => _adminCheckState();
}
class _adminCheckState extends State<adminCheck> {
User? user= FirebaseAuth.instance.currentUser;
bool isAdmin=false;
void initState() {
checkIfAdmin();
super.initState();
}
#override
Widget build(BuildContext context) {
if (isAdmin==true){
countDocuments();
return HomeScreen();
}else{
return notAdmin();
}
}
void checkIfAdmin() async{
DocumentSnapshot currentUser= await FirebaseFirestore.instance.collection('Administrator')
.doc(user!.email)
.get();
if(currentUser.exists){
print("This user is admin");
setState((){
isAdmin=true;
});
}
if(!currentUser.exists){
print("This user is not an admin");
}
}
}
The problem is it returns the notAdmin() class even the void method returns true which supposed to return HomeScreen(), and after few seconds, it will return HomeScreen(). I think there is a delay happening from initializing the data coming from the Firestore. Please help me with my problem. Or is there a way so that it will wait to be initialized first before returning any of those two classes?
The purpose of initState is to determine the state of the widget when it first renders. This means that you can't use asynchronous information (such as data that you still need to load from Firestore) in initState.
If the widget should only be rendered once the data is available, you should not render it until that data has been loaded. You can do this by adding another value to the state to indicate while the document is loading:
class _adminCheckState extends State<adminCheck> {
User? user= FirebaseAuth.instance.currentUser;
bool isAdmin=false;
bool isLoading=true; // 👈
And then use that in rendering:
#override
Widget build(BuildContext context) {
if (isLoading) { // 👈
return CircularProgressIndicator(); // 👈
} else if (isAdmin==true){
countDocuments();
return HomeScreen();
}else{
return notAdmin();
}
}
You can render whatever you want while the data is loading of course (or nothing at all), but the CircularProgressIndicator is typically a reasonable default.
And finally of course, you'll want to clear the loading indicator when the isAdmin is set:
void checkIfAdmin() async{
DocumentSnapshot currentUser= await FirebaseFirestore.instance.collection('Administrator')
.doc(user!.email)
.get();
if(currentUser.exists){
setState((){
isAdmin=true;
isLoading=false; // 👈
});
}
if(!currentUser.exists){
print("This user is not an admin");
}
}
This pattern is so common that it is also encapsulated in a pre-built widget. If that sounds appealing, you'll want to look at using a FutureBuilder.
New to booth flutter and stackoverflow.
I am making the account verification functionally for my flutter app. My plan is to divided this functionally into two parts, part one shows an alertdialog when the screen is built, and part two checks if the "activated" field in firestore is true or false. I have problem of making part two.
This is what I write for part one
String uid = "fdv89gu3njgnhJGBh";
bool isActivated = false;
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
if (isActivated == false) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: AlertDialog(
title: Text("Activation pending"),
content: Text("Your account is waiting to be activate by admin"),
actions: [
FlatButton(
child: Text("Refresh"),
onPressed: () {
// just bring reassurance to user
},
),
],
),
);
});
}
});
}
For part two I plan to make a Future return type function, what it will do is to subscribe the boolean value that stored in firestore: /user/uid/activated, once the function gets a "true" from firestore, it will return it to part one and part one will close the alertdialog(which I haven't figure out how to do this).
I've already seen some solutions from the internet but most solutions involve StreamBuilder, but it seems that I don't need to build any widget for the stream in part two. Is it better to just make changes to what I write previously* or integrate both parts two one StreamBuilder function?
*What I wrote for get the data from one field among all files (and this works well):
Future<bool> registeredCheck(String email) async {
var userInfo = await _firestore.collection("user").get();
for (var userInf in userInfo.docs) {
if (userInf.data()["email"] == email) {
return true;
}
}
return false;
}
}
Thank you
You don't have to query the entire collection. Since you already know the uid, you can just get the document of the uid directly like this:
Future<bool> registeredCheck(String email) async {
final userDoc = await _firestore.collection("user").doc(uid).get();
return userDoc.data()['activated'] ?? false;
}
The reason why I am adding ?? false is to return false instead of null when the activated value is null;
I'm quite new to Flutter and I've been struggling to access a user's document on Firestore.
On the profile page,
I'm setting the current user's UID inside initState, but uid returns null for a quick second, then the page updates with correct info.
So I am able to retrieve a certain field (like displayName), but it isn't quite the best practice. I don't want to have a bunch of boilerplate code and await functions mixed with UI and such.
Code:
FirebaseUser user;
String error;
void setUser(FirebaseUser user) {
setState(() {
this.user = user;
this.error = null;
});
}
void setError(e) {
setState(() {
this.user = null;
this.error = e.toString();
});
}
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then(setUser).catchError(setError);
}
Then in my body I have a Stream builder to get the document.
body: StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(user.uid)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.deepOrange),
),
);
} else {
var userDocument = snapshot.data;
return showProfileHeader(userDocument);
}
},
)
I want to make 'global' references to be accessed throughout the app. Instead of getting the user's id on every page and streaming a specific field when I might need multiple ones.
The only ways I found online to do something similar, created lists with all the data in it. I feel like this might get extra fields I don't need.
How can I make data from Firestore available across the app?
I am using the "Provider" package for doing state management across my app. Nowadays its also the suggested way by the google flutter team when it comes to state management. See the package here: https://pub.dev/packages/provider
Regarding Firebase Auth and accessing the credentials application wide, i am using that said package like stated on this page:
https://fireship.io/lessons/advanced-flutter-firebase/
Short version below. Bootstrap your app like so:
import 'package:provider/provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Make user stream available
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged),
// not needed for your problem but here you can see how
// to define other Providers (types) for your app.
// You need a counter class which holds your model of course.
ChangeNotifierProvider(builder: (_) => Counter(0)),
],
// All data will be available in this child and descendents
child: MaterialApp(...)
);
}
}
Then in your child widgets, just do:
// Some widget deeply nested in the widget tree...
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<FirebaseUser>(context);
return Text(user.displayName) // or user.uid or user.email....
}
}
This should do the trick.
That happens because FirebaseAuth.instance.currentUser() returns a future, and until that future is completed, you will not have the proper FirebaseUser object.
Making the user object global is not a bad idea. In addition, you can hook it up to the FirebaseAuth stream so that it gets updated everytime the user auth status changes, like so in a user.dart file:
class User {
static FirebaseUser _user;
static get user => _user;
static void init() async {
_user = await FirebaseAuth.instance.currentUser();
FirebaseAuth.instance.onAuthStateChanged.listen((firebaseUser) {
_user = firebaseUser;
});
}
}
You can call User.init() in main() and access the user object with User.user.