Flutter ChangeNotifier, duplicate entries to list inside initState method - flutter

I have a Provider in my iniState(){...} of my list view page. If logged in, the user is redirected to this page automatically. The issue is that the initState(){...} is being called more than once and my list view has duplicate entries from firestore. Here is the code I have tried.
It appears to be working on initial load, but if I hot restart it duplicates the items in it
MaterialApp
home: FutureBuilder(
future: Provider.of<AuthService>(context, listen: false).getUser(),
builder: (context, AsyncSnapshot<User> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.error != null) {
print("error");
return Text(snapshot.error.toString());
}
return snapshot.hasData ? PropertyListPage() : WelcomePage();
} else {
return ProgressLoader(
text: 'Loading, please wait...',
);
}
}),
List Screen initState(){...}
#override
void initState() {
super.initState();
Provider.of<PropertyListViewModel>(context, listen: false)
.fetchAllProperties();
}
list view model (Future) to get listings
Future<void> fetchAllProperties() async {
User user = FirebaseAuth.instance.currentUser;
await propertyCollection
.where('emailAddress', isEqualTo: user.email)
.get()
.then((snapshot) {
snapshot.docs.asMap().forEach(
(key, queryDocumentSnapshot) {
properties.add(Property.fromSnapshot(queryDocumentSnapshot));
},
);
});
notifyListeners();
}
This is where I set the ChangeNotifier
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(ChangeNotifierProvider<AuthService>(
create: (context) => AuthService(),
child: MyApp(),
));
}

Related

I am unable to access document field from cloud firestore and show it in a statefulwidget class in flutter

I want to acess the uname field of the current loggedin user .
I added uname in the registration screen like this :
onPressed: () async {
try {
final newuser = await FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: email ?? 'error',
password: password ?? 'error',
);
await FirebaseFirestore.instance
.collection('Users')
.add({' uname': username});
if (newuser != null) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => home()),
);
}
} catch (e) {
print(e);
}
}
But I dont know how to acess it from another file or more specifically I want to acess it on the profile screen .
How can I acess the uname field from firestore in flutter?
You should call the document in the initState and set the value of string using setState
Code:
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
#override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
String? name;
#override
void initState() {
FirebaseFirestore.instance
.collection('Users')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get()
.then((value) {
print(value.data()?[' uname'];
setState(() {
name = value.data()?[' uname'];
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child:
name != null ? Text(name!) : const CircularProgressIndicator()),
);
}
}
Note: This is not recommended ⚠
As future method is called inside the initState(){} it is heavily discouraged to do so as it slows down the process of building the widget and also not considered as a good practice.
Use FutureBuilder for this:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder<DocumentSnapshot<Map<String, dynamic>>>(
future: FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text('Error = ${snapshot.error}');
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
Map<String, dynamic> data = snapshot.data!.data()!;
return Text(data['fullName']); //👈 Your valid data here
},
)),
);
}
I think the better way to what you want is to set the document ID in the collection("Users") same as the uid of the user authenticated. So fetching details or in this case, uname will be easier.
For creating doc with docID same as user uid:
await FirebaseFirestore.instance
.collection('Users')
.doc(newUser.uid)
.add({' uname': username});
For fetching details:
final userData = await FirebaseFirestore.instance
.collection("Users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.get();

Flutter: onGenerateRoute() of a nested Navigator isn't called from within a StreamBuilder

I'm trying to implement redirecting to the login/main page when the login state is changed, but when the login state is changed, nothing happens. Here is the code:
return Scaffold(
body: StreamBuilder<AuthState>(
stream: Auth.instance.stateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
assert(snapshot.connectionState != ConnectionState.done);
if (snapshot.data == AuthState.loggedIn) {
return Navigator(
onGenerateRoute: (_) {
return MaterialPageRoute(
builder: (context) => const MainWidget(),
);
}
);
}
if (snapshot.data == AuthState.loggedOut) {
return Navigator(
onGenerateRoute: (_) {
return MaterialPageRoute(
builder: (context) => const LoginWidget(),
);
}
);
}
if (snapshot.data == AuthState.verifyEmail) {
return const EmailNotVerWidget();
}
throw Exception('Unknown Auth State');
}
),
);
Here, Auth.instance.stateChanges() is just a wrapper for the corresponding FirebaseAuth stream with additional logic.
As the result of debugging, it turned out that, when the login state is changed, the corresponding Navigator widget is returned but its onGenerateRoute() method isn't called, however it's called for the first time when the app is loading. I'm new in Flutter.
Upd. Here is the content of Auth and AuthState:
enum AuthState {
loggedIn,
loggedOut,
verifyEmail,
}
class Auth {
static final Auth _instance = Auth();
static Auth get instance => _instance;
UserObject? user;
String getCurUserId() {
return FirebaseAuth.instance.currentUser!.uid;
}
Future<void> login(String email, String password) async {
await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
}
Future<void> logOut() async {
await FirebaseAuth.instance.signOut();
}
Future<void> register(String email, String password) async {
final credential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
await credential.user!.sendEmailVerification();
}
Future<void> sendEmailVerEmailCurUser() async {
await FirebaseAuth.instance.currentUser?.sendEmailVerification();
}
Future<void> sendPasswordResetEmail(String email) async {
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
}
Stream<AuthState> stateChanges() async* {
await for (final user in FirebaseAuth.instance.authStateChanges()) {
if (user == null) {
//this.user = null;
yield AuthState.loggedOut;
} else {
/* To Do: implement runtime detection of email verification */
if (!user.emailVerified) {
user.reload();
}
if (user.emailVerified) {
/*try {
this.user = await UserObject.loadData(user.uid);
} catch (e) {
print(e.toString());
}*/
yield AuthState.loggedIn;
} else {
yield AuthState.verifyEmail;
}
}
}
}
}
remove the onGeneratedRuote; Not just it can be called during rebuild
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<AuthState>(
stream: Auth.instance.stateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
assert(snapshot.connectionState != ConnectionState.done);
if (snapshot.data == AuthState.loggedIn) {
return const MainWidget()
}
if (snapshot.data == AuthState.loggedOut) {
return const LoginWidget();
}
if (snapshot.data == AuthState.verifyEmail) {
const EmailNotVerWidget();;
}
throw Exception('Unknown Auth State');
})
// })
);
}

I want anonymous authenticated users to be redirected to another page

I want to redirect anonymous users to "GuestHomePage" instead of "HomePage" when they launch the app. 
In my app, authenticated users have their own documents in the "users" collection in the firestore. Therefore, I thought it would be possible to transition the user to another "GuestHomePage" if the user did not have the document in the firestore. But it didn't work. Below is my code. I would like to know what improvements you would like to see.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
MobileAds.instance.initialize();
runApp((MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ListenableProvider<UserState>(
create: (_) => UserState(),
builder: (context, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox();
}
if (snapshot.hasData) {
String uid = FirebaseAuth.instance.currentUser!.uid;
try {
FirebaseFirestore.instance
.collection("users")
.doc(uid)
.get()
.then(
(doc) {},
);
return HomePage();
} catch (e) {
return GuestHomePage();
}
}
return const UserLogin();
},
),
theme: ThemeData(primarySwatch: Colors.teal),
);
},
);
}
}
class UserState extends ChangeNotifier {
User? user;
void setUser(User newUser) {
user = newUser;
notifyListeners();
}
}
try this one...
if (snapshot.hasData) {
String uid = FirebaseAuth.instance.currentUser!.uid;
try {
FirebaseFirestore.instance.collection("users").doc(uid).get().then((doc) {
var data = doc.data();
if (data != null) {
return HomePage();
} else {
return GuestHomePage();
}
},
);
} catch (e) {
return GuestHomePage();
}
}

Error: Class 'String' has no instance getter 'token'. I/flutter ( 3268): Receiver: "dc9e0de8fa2eaa917657e810db06aad2458e4f65"

I have been struggling with this problem for like two days. My social media app should save its state, when signed in so that when you leave the app and come back again it should start from the home page, not the sign in page. I have found that it is possible to do this with StreamBuilder and FutureBuilder. I have tried some things with FutureBuilder and I have some errors.
Below is how my main page looks like:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => UserData(),
child: MaterialApp(
title: 'Curtain App',
debugShowCheckedModeBanner: false,
home: FutureBuilder(
future: SharedPreferencesHelper.getPrefs(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasData) {
Provider.of<UserData>(context).currentUserId =
snapshot.data.token;
return HomeScreen();
} else {
return LoginScreen();
}
},
),
),
);
}
}
class SharedPreferencesHelper {
static final String _tokenCode = "token";
static Future<String> getPrefs() async {
final SharedPreferences preferences = await SharedPreferences.getInstance();
return preferences.getString(_tokenCode) ?? "empty";
}
}
And this is my LoginPage submit btn code:
_submit() async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
// logging in the user w/ Firebase
//AuthService.login(_email, _password);
var user = await DatabaseService.loginUser(_username, _password);
final data = json.decode(user);
SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
print("Hi ${data['username']}");
print("Status ${data['status']}");
print("Token ${data['token']}");
if (data['username'] != null) {
setState(() {
_message = "Hi ${data['username']}";
sharedPreferences.setString('token', data['token']);
});
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
builder: (context) => HomeScreen(),
),
(Route<dynamic> route) => false);
}
}
}
Any ideas on how to solve this ?
Just remove the .token from the line where the error occurs. snapshot.data already is the token.

Strange FirebaseAuth.instance.onAuthStateChanged is not updating streambuilder

I am using firebase authetication in flutter application.When user signup or login i can see that FirebaseAuth.instance.onAuthStateChanged is called
but Streambuilder is not updating the widget. I also noticed that sometime FirebaseAuth.instance.onAuthStateChanged is not even called after user login. But when i reload the screen or rerun the app i can see that user is logged in. Below is my streambuilder code.
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (ctx, userSnapshot) {
if (userSnapshot.hasData) {
print(userSnapshot.data);
print('data changed');
return FutureBuilder(
future: Future.delayed(Duration(seconds: 1)),
builder: (ctx, asyncdata) {
if (asyncdata.connectionState == ConnectionState.done) {
print('user has data');
return UserList();
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
},
);
} else {
print('load auth screen');
return AuthScreen();
}
},
);
Probably you do signIn with a FirebaseAuth that you don't assign in onAuthStateChanged, so you should do like this:
_auth = FirebaseAuth.instance;
///
StreamBuilder<FirebaseUser>(
stream: _auth.onAuthStateChanged,
///
signIn() async {
var result = await _auth.signInWithCredential(credential);
}