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

[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.

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
}
});
}

How to throw error inside riverpod future provider and catch it on error flutter

final loginProvider =
FutureProvider.family<bool, LoginParam>((ref, param) async {
if (param.sgId == '' || param.password == '') {
return false;
}
final http.Response response =
await APIClient().login(param.sgId, param.password);
if (response.statusCode == 200) {
await APIClient().saveTokens(response);
UserDefaultEntity entity =
await ref.watch(userDefaultsProvider(param.sgId).future);
//ref.state = AsyncValue.data(true);
return true;
} else {
throw Exception(jsonDecode(response.body)['message'] ?? 'Unknown Error');
}
});
void login(String userName, String password) async {
state = AsyncValue.loading();
AsyncValue<bool> result;
try {
result = await ref.refresh(loginProvider(LoginParam(userName, password)));
state = result;
} catch (e) {
state = AsyncError(e);
}
}
I'm trying to throw an custom exception inside riverpod future provider and catch the exception in other state notifier classes, but the catch block is not triggered.
Is there any other way to handle exceptions that future provider throw.
First of all, you won't have to manually catch errors inside a FutureProvider, it will do that for you. Refer this example.
Generally, the operations that happen after certain "user interaction" like a button click (in this case, login operation), are not meant to be written in FutureProvider. Scenarios where you'd be using FutureProvider are as follows:
Fetching some data over HTTP/HTTPS.
Performing operations like reading a file or a local database.
So your use case of login can be achieved using a StateNotifier.
// auth_provider.dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
// Always prefer some strongly typed object to
// know current status of authentication.
enum AuthState {
unauthenticated,
authenticated,
authenticating,
failed,
}
// StateNotifier is recommended to encapsulate all your business
// logic into a single class and use it from there.
class AuthStateNotifier extends StateNotifier<AuthState> {
// Initialize with the default state of "unauthenticated".
const AuthStateNotifier() : super(AuthState.unauthenticated);
Future<void> login(LoginParam params) async {
if (param.sgId.isEmpty || param.password.isEmpty) {
state = AuthState.failed;
return;
}
final http.Response response = await APIClient().login(param.sgId, param.password);
if (response.statusCode == 200) {
await APIClient().saveTokens(response);
UserDefaultEntity entity = await ref.watch(userDefaultsProvider(param.sgId).future);
state = AuthState.authenticated;
return;
} else {
state = AuthState.failed;
throw Exception(jsonDecode(response.body)['message'] ?? 'Unknown Error');
}
}
}
// Finally, create a provider that can be consumed in the presentation layer (UI).
final authProvider = StateNotifierProvider<AuthStateNotifier, AuthState>((ref) => const AuthStateNotifier());
Then, in your UI part, usually in the onTap / onPressed event handler of button, you can use it as follows. Please note that, we have created a button widget that extends the ConsumerWidget to access the ref.
// login.dart
import 'auth_provider.dart';
class LoginButton extends ConsumerWidget {
final LoginParam params;
const LoginButton({
Key? key,
required this.params,
}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
void login() {
try {
await ref.read(authProvider.notifier).login(params);
} catch (e) {
// Handle error here.
}
}
return ElevatedButton(
child: Text('Login'),
// Call the handler here.
onPressed: login,
);
}
}

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();
}
}

I have errors regarding positional arguments which I dont quite understand. Help please

I am working on a community app which has series of Tabs on the bottom bar. I've been able to implement the major code and all tabs are working except the last. It is meant to show user profile on click but I'm getting errors due to positional arguments and I cant quite wrap my head around the solution. Here is the bit of code giving me the error(extracted from my main.dart file):
providers: [
ChangeNotifierProvider<AppState>(create: (_) => AppState()),
ChangeNotifierProvider<AuthState>(create: (_) => AuthState()),
ChangeNotifierProvider<FeedState>(create: (_) => FeedState()),
ChangeNotifierProvider<ChatState>(create: (_) => ChatState()),
ChangeNotifierProvider<SearchState>(create: (_) => SearchState()),
ChangeNotifierProvider<NotificationState>(
create: (_) => NotificationState()),
***ChangeNotifierProvider<ProfileState>(create: (_) => ProfileState()),***
],
I'm getting the errors on the last line there: 1 positional argument(s) expected, but 0 found.
Try adding the missing arguments.
Here is a portion of my code from Profile State which I believe is most relevant:
import 'package:firebase_database/firebase_database.dart' as dabase;
class ProfileState extends ChangeNotifier {
ProfileState(this.profileId) {
databaseInit();
userId = FirebaseAuth.instance.currentUser.uid;
_getloggedInUserProfile(userId);
_getProfileUser(profileId);
}
/// This is the id of user who is logegd into the app.
String userId;
/// Profile data of logged in user.
UserModel _userModel;
UserModel get userModel => _userModel;
dabase.Query _profileQuery;
StreamSubscription<Event> profileSubscription;
/// This is the id of user whose profile is open.
final String profileId;
/// Profile data of user whose profile is open.
UserModel _profileUserModel;
UserModel get profileUserModel => _profileUserModel;
bool _isBusy = true;
bool get isbusy => _isBusy;
set loading(bool value) {
_isBusy = value;
notifyListeners();
}
databaseInit() {
try {
if (_profileQuery == null) {
_profileQuery = kDatabase.child("profile").child(profileId);
profileSubscription = _profileQuery.onValue.listen(_onProfileChanged);
}
} catch (error) {
cprint(error, errorIn: 'databaseInit');
}
}
bool get isMyProfile => profileId == userId;
/// Fetch profile of logged in user
void _getloggedInUserProfile(String userId) async {
kDatabase
.child("profile")
.child(userId)
.once()
.then((DataSnapshot snapshot) {
if (snapshot.value != null) {
var map = snapshot.value;
if (map != null) {
_userModel = UserModel.fromJson(map);
}
}
});
}
/// Fetch profile data of user whoose profile is opened
void _getProfileUser(String userProfileId) {
assert(userProfileId != null);
try {
loading = true;
kDatabase
.child("profile")
.child(userProfileId)
.once()
.then((DataSnapshot snapshot) {
if (snapshot.value != null) {
var map = snapshot.value;
if (map != null) {
_profileUserModel = UserModel.fromJson(map);
Utility.logEvent('get_profile');
}
}
loading = false;
});
} catch (error) {
loading = false;
cprint(error, errorIn: 'getProfileUser');
}
}
Please what's the solution ?
You have the this.profileId as an argument in the ProfileState constructor, and in the providers in the main file you didnt provide the needed argument for the constructor. That's why you are getting this error I believe.
flutterdart

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