Provider keeps data after logout - flutter

In my first screen, I consume data from Firebase and store it in provider.
This data will be used in another screen.
#override
void initState() {
super.initState();
FirestorePreviousPage.documentsListDescending().then((query) {
final totalHisabProvider = Provider.of<TotalHisab>(context, listen: false);
totalHisabProvider.addItemsFromSnapshot(query);
final unpaidHisabProvider = Provider.of<UnpaidHisab>(context, listen: false);
unpaidHisabProvider.addItems(query);
final paidHisabProvider = Provider.of<PaidHisab>(context, listen: false);
paidHisabProvider.addItems(query);
});
}
The documentsListAscending method:
static Future<QuerySnapshot> documentsListDescending() async {
final user = await CurrentUser.getCurrentUser();
return await Firestore.instance
.collection(user.uid)
.orderBy('serverTimestamp', descending: true)
.getDocuments();
}
My logout method:
static Future<void> logout(BuildContext context) async {
await FirebaseAuth.instance.signOut();
Navigator.of(context).pushNamedAndRemoveUntil(
WelcomeScreen.routeName,
(Route<dynamic> route) => false,
);
}
Now when I do logout and login with a different user, this new user still has access to the data of the previous user. Now I think I understand this part. As long as the app is working, it will keep the data in the state management.
But why doesn't the retrieve the new data from Firebase?
Shouldn't this line update data with the new user ID: .collection(user.uid)
I have an idea of how to solve this. Just clear the data in all the providers in the logout method. But that doesn't feel right. I need to understand why it isn't working first.

I ended up clearing the providers manually:
static Future<void> logout(BuildContext context) async {
await FirebaseAuth.instance.signOut();
// Clear all providers
Provider.of<Cart>(context, listen: false).clear();
Provider.of<UnpaidHisab>(context, listen: false).clear();
Provider.of<PaidHisab>(context, listen: false).clear();
Provider.of<TotalHisab>(context, listen: false).clear();
Navigator.of(context).pushNamedAndRemoveUntil(
WelcomeScreen.routeName,
(Route<dynamic> route) => false,
);
}
It works just fine as far I can tell.

Related

shared_preferences values returning null in flutter

I am using shared_preferences to store a bool value locally but I think I am doing something wrong.
So first of all, here is my initState:
#override
initState(){
super.initState();
checkIfUserHasData();
getBoolValuesSF();
}
on checkIfUserHasData, Im calling another function at the end (addBoolToSF)
Future<void> checkIfUserHasData ()async {
var collection = FirebaseFirestore.instance.
collection('users').doc(userID).collection('personalInfo');
var querySnapshots = await collection.get();
for (var snapshot in querySnapshots.docs) {
documentID = snapshot.id;
}
await FirebaseFirestore.instance
.collection('users')
.doc(userID)
.collection('personalInfo').doc(documentID)
.get().then((value) {
if (!mounted) return;
setState(() {
gender = value.get('gender');
profileImageUrl = value.get('url');
print(profileImageUrl);
print(gender);
});
});
if (gender != null){
if (!mounted) return;
setState((){
isUserNew = false;
});
if(gender == "Male"){
setState(() => genderIsMale = true);
addBoolToSF();
}else{
setState(() => genderIsMale = false);
addBoolToSF();
}
}else {
return;
}
}
Then addBoolToSF:
addBoolToSF() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool('genderType', genderIsMale);
}
Lastely getBoolValuesSF:
getBoolValuesSF() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
bool _genderType = ((prefs.getBool('genderType') ?? true)) ;
genderType = _genderType;
});
}
When the genderType value is obtained I then decide which image to be the background image on the screen:
CachedNetworkImage(
placeholder: (context, url) =>
CircularProgressIndicator(),
imageUrl: genderType ? // : //
With all of that said, here is what is happening when the gender is changed on the firebase firestore:
The first time I navigate or refresh the screen nothing is changed and I get this error:
type 'Null' is not a subtype of type 'bool'
The second time I refresh or navigate to the screen, I do get the correct image on place but I get the same error message again
type 'Null' is not a subtype of type 'bool'
I have tried several ways to solve this issue but i dont seem to get it right.
Edit: I have noticed that when I removed the last part for CachedNetworkImage, I get no error so I think the problem might be on this part
In case like that when you need to wait for a future to build some UI, the go to way is to use a FutureBuilder
You use it like this
FutureBuilder<bool>(
future: getBoolValuesSF,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
// build your UI here based on snapshot value
},
)
checkIfUserHasData() and getBoolValuesSF() both are future method. you can create another async method and put it inside initState.
#override
initState(){
super.initState();
newMthod();
}
newMthod() async{
await checkIfUserHasData();
await getBoolValuesSF();
}

Flutter Web stream asyncMap is not calling

I am trying to calling this function from Stream builder on Mobile version(Android & IOS) it's working properly but in web asyncMap function is not working, please, what would be the possible reason behind it or is there any alternative to this?
Stream<_TalentDetailsViewModel> getViewModelStream(
BuildContext context, String matchId) {
final talents = Provider.of<Talents>(context, listen: false);
print("Printing properly");
return Provider.of<Matches>(context, listen: false)
.getStreamById(matchId).asyncMap((Match? match) async {
print("Not Printing");
final talent = await talents.getById(match!.talent!);
return _TalentDetailsViewModel(match, talent!);
});
}

Riverpod's StreamProvider yields StreamValue only once | Flutter & Hive

I wrote a StreamProvider that I listen to right after startup to get all the information about a potentially logged in user. If there is no user, so the outcome would be null, the listener stays in loading state, so I decided to send back a default value of an empty user to let me know that the loading is done.
I had to do this, because Hive's watch() method is only triggered when data changes, which it does not at startup.
So after that, I want the watch() method to do its job, but the problem with that, are the following scenarios:
At startup: No user - Inserting a user -> watch method is triggered -> I get the inserted users data -> Deleting the logged in user -> watch method is not triggered.
At startup: Full user - Deleting the user -> watch method is triggered -> I get an empty user -> Inserting a user -> watch method is not triggered.
After some time I found out that I can make use of all CRUD operations as often as I want to and the Hive's box does what it should do, but the watch() method is not triggered anymore after it got triggered once.
The Streamprovider(s):
final localUsersBoxFutureProvider = FutureProvider<Box>((ref) async {
final usersBox = await Hive.openBox('users');
return usersBox;
});
final localUserStreamProvider = StreamProvider<User>((ref) async* {
final usersBox = await ref.watch(localUsersBoxFutureProvider.future);
yield* Stream.value(usersBox.get(0, defaultValue: User()));
yield* usersBox.watch(key: 0).map((usersBoxEvent) {
return usersBoxEvent.value == null ? User() : usersBoxEvent.value as User;
});
});
The Listener:
return localUserStream.when(
data: (data) {
if (data.name == null) {
print('Emitted data is an empty user');
} else {
print('Emitted data is a full user');
}
return Container(color: Colors.blue, child: Center(child: Row(children: [
RawMaterialButton(
onPressed: () async {
final globalResponse = await globalDatabaseService.signup({
'email' : 'name#email.com',
'password' : 'password',
'name' : 'My Name'
});
Map<String, dynamic> jsonString = jsonDecode(globalResponse.bodyString);
await localDatabaseService.insertUser(User.fromJSON(jsonString));
},
child: Text('Insert'),
),
RawMaterialButton(
onPressed: () async {
await localDatabaseService.removeUser();
},
child: Text('Delete'),
)
])));
},
loading: () {
return Container(color: Colors.yellow);
},
error: (e, s) {
return Container(color: Colors.red);
}
);
The CRUD methods:
Future<void> insertUser(User user) async {
Box usersBox = await Hive.openBox('users');
await usersBox.put(0, user);
await usersBox.close();
}
Future<User> readUser() async {
Box usersBox = await Hive.openBox('users');
User user = usersBox.get(0) as User;
await usersBox.close();
return user;
}
Future<void> removeUser() async {
Box usersBox = await Hive.openBox('users');
await usersBox.delete(0);
await usersBox.close();
}
Any idea how I can tell the StreamProvider that the watch() method should be kept alive, even if one value already got emitted?
but the watch() method is not triggered anymore after it got triggered
once
Thats because after every CRUD you're closing the box, so the stream (which uses that box) stop emitting values. It won't matter if you're calling it from somewhere outside riverpod (await Hive.openBox('users')) its calling the same reference. You should close the box only when you stop using it, I would recommend using autodispose with riverpod to close it when is no longer used and maybe put those CRUD methods in a class controlled by riverpod, so you have full control of the lifecycle of that box
final localUsersBoxFutureProvider = FutureProvider.autoDispose<Box>((ref) async {
final usersBox = await Hive.openBox('users');
ref.onDispose(() async => await usersBox?.close()); //this will close the box automatically when the provider is no longer used
return usersBox;
});
final localUserStreamProvider = StreamProvider.autoDispose<User>((ref) async* {
final usersBox = await ref.watch(localUsersBoxFutureProvider.future);
yield* Stream.value(usersBox.get(0, defaultValue: User()) as User);
yield* usersBox.watch(key: 0).map((usersBoxEvent) {
return usersBoxEvent.value == null ? User() : usersBoxEvent.value as User;
});
});
And in your methods use the same instance box from the localUsersBoxFutureProvider and don't close the box after each one, when you stop listening to the stream or localUsersBoxFutureProvider it will close itself

Dynamic variable in flutter. Any better way to do this?

[Edited] I have this application with multilevel user application where I have functions based on roles. Currently, I am saving user response in shared preferences and fetching it by getting it's instance whenever I need it. And also, I am using different screens and different widgets for each role. But there has to be a better way to do it. I am so confused with singleton pattern and making global variables in dart.
Here's my code:
void main() {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.getInstance().then((prefs) {
var user=prefs.getString("role");
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<RoleNotifier>(
create: (_) => RoleNotifier(user),
),
],
child: MyApp(),
));
});
}
void setRole(String role) async {
Provider.of<RoleNotifier>(context, listen:false).setUser(role);
await SharedPreferences.getInstance().then((prefs){
prefs.setString("role", role);
});
}
_login() async {
try {
setState(() {
_isbusy = true;
});
var data = {"username": _emailc.text, "password": _pass.text};
var response = await CallApi().postData(data, 'login');
SharedPreferences local = await SharedPreferences.getInstance();
var res = response.data;
print(res);
if (res['success']) {
local.setString('token', res['data']['token']);
if (res['data']['role'] == 'admin') {
setRole(res['data']['role']);
local.setString('info', json.encode(res['data']));
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => AdminDashBoard()));
} else if (res['data']['role'] == 'dev') {
setRole(res['data']['role']);
local.setString('post', res['data']['role']);
local.setString('info', json.encode(res['data']));
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => DevDashBoard()));
} else if (res['data']['role'] == 'user') {
setRole(res['data']['role']);
local.setString('post', res['data']['role']);
local.setString('info', json.encode(res['data']));
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => UserDashBoard()));
}
} else {
print('error');
setState(() {
_isbusy = false;
});
showSimpleFlushbar(context, "An Error Occurred!");
}
} on DioError catch (e) {
print(e);
setState(() {
_isbusy = false;
});
print(e.response.data);
print(e.response.headers);
print(e.response.request);
showSimpleFlushbar(context,
"Login Failed! Please Check your credentials and try again.");
}
}
And to access the variables:
SharedPreferences.getInstance().then((prefs) {
var data = jsonDecode(prefs.getString("info"));
setState(() {
email = data['email'];
post = data['role'];
});
});
The problem is, I have to run this on initState in every screen and there is a delay in fetching data which throws an exception for small time.
I just figured out this is working.
(Provider.of<RoleNotifier>(context).getUser()=="admin")?AdminWidget():SizedBox(),
Now I can access the data from anywhere using provider. But is there any better way to do this? I've heard a lot about singleton pattern and in my case even though it works, it seems like I am doing something wrong. Like I am listening to the value that is static immediately after login is completed.
SharedPreferences prefs;// file level global variable
main(){
SharedPreferences.getInstance().then((p)=>prefs = p);
// do whatever
runApp(MyApp());
}
Now, don't use SharedPreferences.getInstance() when needed but use the global variable
created.
Like
prefs.getString('name');
or
prefs.setString('foo','bar');
For example
class Foo extends StatelessWidget{
Widget build(context){
var name = prefs.getString('name');// don't use var prefs = await SharedPreferences.getInstance();
return Text("name is $name");
}
}
Why not create a User class and extend it with Provider?
Then based on the Consumers to build dynamic widgets you can pump out what ever you want based on the User.role for the selected user.
In your Singleton you can add a Singleton().selectedUser var and once a user logs in or what ever process they follow you can assign it to that. Use this selectedUser var for your Provider.value.
If you need example code let me know.

Can't Logout and redirect to LoginPage when using Firebase/ Google Sign-in in Flutter

I am working on implementing a Flutter app that uses Firebase authentication as well as Google-Sign. I have successfully gotten firebaseauth working to sign-in/signout without issue. based on the authstate change, I direct to main page or login page. I then implemented Google Sign-in, which auto-signs in user to firebase.
This worked and I got the necessary permissions to be prompted,etc. But when I click to logout, the app doesn't redirect to login screen. I Am calling the await firebaseAuth.instance.Signout() ...and I believe it does remove the user..but it doesn't redirect. Then, when I try to sign-in again, nothing happens.
I have tried many different variations of logging in..I can't seem to figure out why it won't redirect on logout.
This is my Sign-in Logic which signs into Google and firebase:
try {
final GoogleSignInAccount googleSignInAccount =
await _googleSignIn.signIn();
final GoogleSignInAuthentication googleSignInAuthentication =
await googleSignInAccount.authentication;
final AuthCredential credential = GoogleAuthProvider.getCredential(
accessToken: googleSignInAuthentication.accessToken,
idToken: googleSignInAuthentication.idToken,
);
final FirebaseUser user = await _firebaseAuth.signInWithCredential(credential);
assert(!user.isAnonymous);
assert(await user.getIdToken() != null);
final FirebaseUser currentUser = await _firebaseAuth.currentUser();
assert(user.uid == currentUser.uid);
} catch (error) {
print(error);
}
notifyListeners();
}
This is my logic in main.dart to redirect to appropriate page:
home: StreamBuilder(
stream: _firebaseAuth.onAuthStateChanged,
builder: (context, snapshot) {
if(snapshot.hasData){
return HomePage();
} else {
return Login();
}
},
)
This is my logic to Signout()
Future<void> signOut() async {
await _firebaseAuth.signOut().catchError((error){
print(error.toString());
});
}
In my case, I don't want to log the user out of Google, just my app.
But I cannot figure out why this doesn't redirect. It worked correctly when I was using the standard firebase sign-in with email/password.
After a lot of digging around, it turns out that if you are calling SignOut() from the sideDrawer component, you can't just call Signout. you need to do this. Solved it for me.
FlatButton(
child: Text('Logout'),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pushReplacementNamed('/');
Provider.of<Auth>(context).signOut();
},
),
This works in FlutterFire. I used authStateChanges() method on a FirebaseAuth instance and Navigator.
authStateChanges() method allows you to get the user's authentication state.
Firebase Auth enables you to subscribe in realtime to this state via a
Stream. Once called, the stream provides an immediate event of the
user's current authentication state, and then provides subsequent
events whenever the authentication state changes.
Navigator is for navigating to main.dart
To switch to a new route, use the Navigator.push() method. The push()
method adds a Route to the stack of routes managed by the Navigator.
main.dart
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:flutter/material.dart';
import 'menu.dart';
import 'login.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:firebase_core/firebase_core.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitUp]);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TestApp',
theme: ThemeData(primarySwatch: Colors.blue),
home:
StreamBuilder<auth.User>(
stream: auth.FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<auth.User> snapshot) {
if(snapshot.hasData) {
print("There is a user logged in");
return HomePage();
}
else {
return LoginPage();
}
},
)
);
}
}
Sign out using Firebase Authentication
Future<void> signOut() async {
await _firebaseAuth.signOut().catchError((error){
print(error.toString());
});
Navigator.push(context, MaterialPageRoute(builder: (context) => MyApp()));
}
I think it's only to do singOut on _googleSignIn like this:
Future<void> signOut() async {
await _firebaseAuth.signOut().catchError((error){
print(error.toString());
});
await _googleSignIn.signOut();
}