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

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

Related

How to ensure that a future completes and all its sub future calls before continue execution

I have a method that uploads a photo to firebase cloud storage and after that I get the download url for the photo and then update the firebase database document with that url.
My problem here in the ElevatedButton callback when I use uploadProfilePhoto(..).then the code is executed before setPersonalPhotoUrl() method completes its job and set personalPhotoUrl.
I tried to use whenComplete instead but it didn't work. My thought if not mistaken is that uploadProfilePhoto(..).then is completing its future but it does not take into account the completion of that future method setPersonalPhotoUrl(). I need help with this.
fields declared:
UploadTask? uploadTask;
String personalPhotoUrl = '';
the update button:
ElevatedButton(
child: Text('Update Info'),
onPressed: () async {
await uploadProfilePhoto(profilePhotoFile).then((value) async {
// Create an instance of ServiceProvider
final SP = ServiceProvider(
id: currentUserUid!,
name: _controllerName.text.trim(),
last: _controllerLast.text.trim(),
mobile: _controllerMobile.text.trim(),
bio: _controllerBio.text.trim(),
photo: personalPhotoUrl, //problem here the value is ''
serviceId: _selectedServiceId!,
subServices: _selectedSubServicesList,
);
// Create or Update the service provider
try {
await DbServices(uid: currentUserUid!)
.updateSProviderData(SP)
.then((value) async {
// update the customers collection when the future completes.
final customer = Customer(
uid: currentUserUid!,
name: _controllerName.text.trim(),
isServiceProvider: true);
await DbServices(uid: currentUserUid!).updateCustomer(customer);
// update the user displayname in firebaseauth when the future completes.
final user = await FirebaseAuth.instance.currentUser;
if (user != null) {
await user.updateDisplayName(_controllerName.text.trim());
}
});
} catch (e) {
Utils.ShowSnackBar(e.toString());
}
});
Utils.ShowSnackBar('Updated successfully');
Navigator.maybePop(context).then((value) {
if (value == false) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => Home(),
));
}
});
})
This is upload photo method which uploads the given photo to FiresStore cloud storage:
Future uploadProfilePhoto(File? photoFile) async {
if (photoFile == null) return;
const path = 'images/profile_photo.jpg';
final storageRef = FirebaseStorage.instance.ref().child(path);
try {
uploadTask = storageRef.putFile(photoFile);
uploadTask?.snapshotEvents.listen((TaskSnapshot taskSnapshot) async {
switch (taskSnapshot.state) {
....
case TaskState.success:
setPersonalPhotoUrl(storageRef);
break;
}
});
} on FirebaseException catch (e) {
// do something
print('ERROR: Exception thrown when uploading the image: $e');
}
}
and this method will be called from within uploadProfilePhoto and set the url:
void setPersonalPhotoUrl(Reference storageRef) async {
personalPhotoUrl = await storageRef.getDownloadURL();
}
I don't won't to update the db document before I make sure that the photo is uploaded and later I want to inform the user that if the photo failed to upload and maybe then set the document field to an empty string
1. Refactor your upload function to.
Future uploadProfilePhoto(
File? photoFile, ValueSetter<TaskSnapshot> resultCallBack) async {
if (photoFile == null) return;
const path = 'images/profile_photo.jpg';
final storageRef = FirebaseStorage.instance.ref().child(path);
try {
UploadTask? uploadTask = storageRef.putFile(photoFile);
uploadTask.snapshotEvents.listen((TaskSnapshot taskSnapshot) async {
resultCallBack(taskSnapshot);
});
} on FirebaseException catch (e) {
// do something
print('ERROR: Exception thrown when uploading the image: $e');
}
}
2. Can then use it like
onPressed: () async {
await uploadProfilePhoto(
profilePhotoFile, (TaskSnapshot taskSnapshotResult) {
// all the results you need are available in taskSnapshotResult
if(taskSnapshotResult.state == TaskState.success){
/// can do what ever you like here
.... // Create an instance of ServiceProvider
final SP = ServiceProvider( ..... blah blah blah
}
});
}

Flutter_Bloc 8 and Firebase Verify Email -- Clear Prior State?

I am trying to utilize FirebaseAuth's verify email functionality in my Flutter app. I'm using Flutter_BLoC 8. The code works, but when I verify the email address by clicking the link in the generated email, the prior state is apparently still buffered in my app and I need to click TWICE on the 'Continue' button to proceed to the Home Screen.
Here's my code:
ElevatedButton(
onPressed: () async {
context.read<EmailVerificationBloc>().add(const IsEmailVerified());
final isVerified = context.read<EmailVerificationBloc>().state;
if (isVerified == const EmailVerificationState.verified()) {
context.router.replace(const HomeRoute());
} else {
showErrorFlash(context, 'Email Is Not Verified');
}
},
child: const Center(
child: Text('Continue'),
),
),
Here's the BLoC event:
Future<void> _onIsEmailVerified(IsEmailVerified event, Emitter emit) async {
final successOrFailure = await _authRepository.isUserEmailVerified();
successOrFailure.fold(
(failure) {
emit(EmailVerificationState.error(failure));
},
(success) {
if (success) emit(const EmailVerificationState.verified());
},
);
}
And lastly, here's the repository method that's being called:
Future<Either<AuthFailure, bool>> isUserEmailVerified() async {
try {
await _currentUser!.reload(); // We will only call on authenticated users, so we don't expect null
return right(_currentUser!.emailVerified);
} on FirebaseAuthException catch (e) {
final error = e.code;
switch (error) {
case 'network-request-failed':
return left(const AuthFailure.noNetworkConnection());
default:
return left(const AuthFailure.unexpectedError());
}
} on PlatformException {
return left(const AuthFailure.unexpectedError());
}
}
Any ideas on how I can, for lack of a better term, flush the prior state? I'm sort of new to Flutter and BLoC, so I'm hopeful it's a relatively easy thing I'm just overlooking.
Thanks in advance.

Added data is only showing after reloading in flutter

here is a popup screen to add the transaction to the app, as you can see here
and when the add button pressed the data will add to database and also to the dislpay , here is the code
ElevatedButton(
//on pressed
onPressed: () async {
final _categoryName = _nameEditingController.text;
if (_categoryName.isEmpty) {
return;
}
final _type = selectedCategoryNotifier.value;
//sending the data to model class
final _category = CategoryModel(
id: DateTime.fromMillisecondsSinceEpoch.toString(),
name: _categoryName,
type: _type,
);
//inserting the data to database
await CategoryDb.instance.insertCategory(_category);
//refreshing the ui
await CategoryDb.instance.refreshUI();
//and quitting the popup screen
Navigator.of(ctx).pop();
},
child: const Text('Add'),
),
and in this code you can see that I called 2 functions that for insert data and also refresh the UI, in the refresh UI function I added the function that to get all data from database to screen, here the code of all functions for CRUD operatins
const databaseName = 'category-database';
abstract class CategoryDbFunctions {
Future<List<CategoryModel>> getCategories();
Future<void> insertCategory(CategoryModel value);
}
//CRUD operations code
class CategoryDb implements CategoryDbFunctions {
CategoryDb._internal();
static CategoryDb instance = CategoryDb._internal();
factory CategoryDb() {
return instance;
}
ValueNotifier<List<CategoryModel>> incomeCategoryListListener =
ValueNotifier([]);
ValueNotifier<List<CategoryModel>> expenseCategoryListListener =
ValueNotifier([]);
#override
Future<void> insertCategory(CategoryModel value) async {
final _categoryDB = await Hive.openBox<CategoryModel>(databaseName);
await _categoryDB.add(value);
await refreshUI();
}
#override
Future<List<CategoryModel>> getCategories() async {
final _categoryDB = await Hive.openBox<CategoryModel>(databaseName);
return _categoryDB.values.toList();
}
Future<void> refreshUI() async {
final _allCategories = await getCategories();
incomeCategoryListListener.value.clear();
expenseCategoryListListener.value.clear();
await Future.forEach(
_allCategories,
(CategoryModel category) {
if (category.type == CategoryType.income) {
incomeCategoryListListener.value.add(category);
} else {
expenseCategoryListListener.value.add(category);
}
},
);
}
}
so I checked the all things , but I couldn't find where I'm missing parts,
and here is the main part, it is adding to the database also displaying after I refresh the UI or change the tab here you can see what I mean by 'changing the tab'
this is the problem I'm trying to fix this for 2 day, i couldn't find any solution or mistake in my code
There many ways you can handle this problem.
but I dont see where you notify youre ui that the data has been changed, flutter does only update the ui when you use setState etc.. these functions help flutter updating the ui where the data changed.
i would recommend you to use setState in the place you invoke youre dialog.
onTap:(){
setState(){
await dialogStuff();
}
}

Flutter Firebase Auth throws NoSuchMethodError: The getter 'data' was called on null

During the email app signup process using firebaseAuth.createUserWithEmailAndPassword, when I try to do an upload or save to prefs in the .then part it throws this error:
NoSuchMethodError: The getter 'data' was called on null.
So I can work around this by Navigating to a new screen and postponing processing of the user's TextFormField input till there, but it's messy and bugs me.
Doing anything big in the .then seems problematic but I don't really know what's causing the problem, or what in fact the best way is to solve this kind of issue for future clarity. Education appreciated!
void registerToFb() {
firebaseAuth
.createUserWithEmailAndPassword(
email: emailController.text, password: passwordController.text)
.then((result) async {
Person user = new Person();
user.email = emailController.text;
user.firstName = firstNameController.text;
user.surname = surnameController.text;
user.postcode = postcodeController.text;
user.password = passwordController.text;
user.city = cityController.text ?? "Edinburgh";
user.firebaseId = result.user.uid;
Map<String, dynamic> firebaseUpload = user.toMap();
print("Attempting to reduce upload");
firebaseUpload.removeWhere((key, value) => value == null);
user.country = "GB";
String path = "${user.country}/${user.city}/People";
print("Attempting record upload");
DocumentReference autoId =
await myFirestore.collection(path).add(firebaseUpload);
user.personId = autoId.id;
user.saveToPrefs(prefs);
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (context) => MyHomePage()));
}).catchError((err) {
print("Login thrown an error...\n${err.toString()}");
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Error 10"),
content: Text("${err.toString()}"),
actions: [
ElevatedButton(
child: Text("Ok"),
onPressed: () {
Navigator.of(context).pop();
},
)
],
);
});
});
A suggestion from me is to completely remove the .then() callback, since you have it stated as async. A better approach would be to make the whole function async, so you can do all your async code directly inside that.
Make the function async
void registerToFb() async { ...
Change the .then() callback to a simple await and store the result in your result variable.
var result = await firebaseAuth.createUserWithEmailAndPassword(email: emailController.text, password: passwordController.text);
I would highly suggest surrounding this statement with a try/catch block, to avoid unhandled errors:
try {
var result = await firebaseAuth.createUserWithEmailAndPassword(
email: emailController.text,
password: passowrdController.text
);
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
print('password too weak.');
} else if (e.code == 'email-already-in-use') {
print('email already exists');
}
} catch (e) {
print(e);
}
You might get this error because you marked the .then() call as async, since it then executes asynchronously and the data might not be "there" yet, but I am not sure about this one.

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.