Flutter Get Navigator context for async task - flutter

When I press the login button, I receive a Future. If true, then I change page, else I display a popup dialog.
onPressed: () async {
final navigator = Navigator.of(context); // store the Navigator to enable .push
bool? res = await sendUser('6#gmail.com', 'password');
// if connection succeeds
if (res) {
navigator.push(MaterialPageRoute(builder: (context) => const newScreen()));
} else {
showPopUp(context);
}
}
I have the warning 'Do not use BuildContexts accros async gaps' for the popup. I had this warning for Navigator.push, so I fixed it by storing the Navigator but I don't know what to do for the popup. Can I get like the context of navigator ?

Try surrounding your showPopUp function inside an if (mounted) like so:
onPressed: () async {
final navigator = Navigator.of(context); // store the Navigator to enable .push
bool? res = await sendUser('6#gmail.com', 'password');
// if connection succeeds
if (res) {
navigator.push(MaterialPageRoute(builder: (context) => const newScreen()));
} else if (mounted) { //Changed here <-------------
showPopUp(context);
}
}
PS: Caution when using ternary operators while this issue is open

Related

Shows warning: Do not use BuildContexts across async gaps

if (_formKey.currentState!.validate()) {
try {
final newUser =
await _auth.createUserWithEmailAndPassword(
email: email.text, password: password.text);
if (newUser != null) {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => DashboardScreen(),
// ));
Navigator.pushNamed(context, 'dashboard');
}
setState(() {});
} catch (e) {
print(e);
}
}
},
this warning shown on Navigator.pushNamed(context,'dashboard');
trying to navigate to the dashboar screen.
1.
You have to put delay for other process can finish till then
Future.delayed(Duration(milliseconds: 200)).then((value) {
Navigator.pushNamed(context, 'dashboard')
});
2.
add if (!mounted) return; before Navigator.pushNamed(context, 'dashboard')
3.
Please put await before the navigator flutter because you used an asynchronously method call so you have to wait until the process is finished then you can navigate to your pages
await Navigator.pushNamed(context, 'dashboard');
4.
Also, you can store your navigator into a var and then use it.
final nav = Navigator.of(context);
nav.pushNamed('dashboard');

How do I use sharedPreferences with booleans?

I am currently learning sharedPreferences and trying to set and get values to check if a button has been clicked or not.
This is my class for sharedPreferences
class UserSimplePrefences {
static SharedPreferences? _preferences;
static const _keyButton = 'buttonStatus';
static Future init() async {
_preferences = await SharedPreferences.getInstance();
}
static Future setButtonStatus(bool btnStatus) async {
await _preferences?.setBool(_keyButton, btnStatus);
}
static bool? getButtonStatus() {
return _preferences?.getBool(_keyButton);
}
}
here in my main.dart
I have a button.
bool? onLineStatus;
//
void initState() {
super.initState();
WidgetsBinding.instance?.addObserver(this);
onLineStatus = UserSimplePrefences.getButtonStatus() !;
displayToastMessage(onLineStatus.toString(), context);
}
//
#override
Widget build(BuildContext context) {
//
RaisedButton(
onPressed: () async {
if (UserOnline! =true) {
UserOnline = true;
await UserSimplePrefences.setButtonStatus(true);
displayToastMessage("You are Online now", context);
} else {
UserOnline =false;
await UserSimplePrefences.setButtonStatus(false);
displayToastMessage("You are Offline now", context);
}}
),
}
UserOnline is to toggle the button, works fine without SharedPreferences.
In more simple language, when I hit the button i.e Online and close the app and reopen the app sometime later I want the init statement to be called with the toast message as true and similarly when I click offline I want the init statement to call the toast false.
Issue: My toast message is always true.
`
add setState to your onPressed
onPressed: () async {
if (UserOnline! =true) {
UserOnline = true;
await UserSimplePrefences.setButtonStatus(true);
displayToastMessage("You are Online now", context);
setState(() {});
} else {
UserOnline =false;
await UserSimplePrefences.setButtonStatus(false);
displayToastMessage("You are Offline now", context);
setState(() {});
}}

Flutter - onStepContinue called automatically on build

I'm using Stepper widget to make a form for profile creation. In the onStepContinue method, if its the last step I put the function call for sending data to backend and added the navigation route to home page to its .whenComplete method.
body: Stepper(
type: StepperType.horizontal,
currentStep: _activeCurrentStep,
steps: stepList(),
onStepContinue: () async {
final isLastStep = _activeCurrentStep == stepList().length - 1;
if (isLastStep) {
final authenticationNotifier =
Provider.of<AuthenticationNotifier>(context, listen: false);
var userEmail =
await authenticationNotifier.fetchUserEmail(context: context);
var profileName = profileNameController.text;
var profileBio = profileBioController.text;
await profileNotifier(false)
.createProfile(
context: context,
profileDTO: ProfileDTO(
useremail: userEmail,
profile_name: profileName,
profile_bio: profileBio,
))
.whenComplete(
() => Navigator.of(context).popAndPushNamed(HomeRoute));
} else if (_activeCurrentStep < (stepList().length - 1)) {
setState(() {
_activeCurrentStep += 1;
});
}
},
onStepCancel: _activeCurrentStep == 0
? null
: () {
setState(() {
_activeCurrentStep -= 1;
});
},
onStepTapped: (int index) {
setState(() {
_activeCurrentStep = index;
});
},
),
The stepper widget is in a page/scaffold of its own. Its loaded from onPressed of a button in authview.dart file.
onPressed: () {
authenticationNotifier.signup(
context: context,
useremail: signupEmailController.text,
userpassword: signupPasswordController.text);
Navigator.of(context)
.pushNamed(ProfileCreationRoute);
},
The problem is that as soon as I press the sign up button in authview the stepper page shows up for a fraction of a second and loads the homepage without letting me create the profile. I need it to just show the stepper and go to homepage only after I fill the profile details and click submit.
I thought .whenComplete would be called only when the button is pressed and its parent function finishes its work, and in this case I guess the problem is somehow with the stepper widget itself.
I also added && profileNameController.text.isNotEmpty in the if (isLastStep) condition but it doesn't work. Clicking on sign up button is bypassing the stepper widget and going to homeview as soon as stepper finishes building itself.
I don't understand what's going on. Please help.
EDIT
The createProfile function in notifier is
class ProfileNotifier extends ChangeNotifier {
final ProfileAPI _profileAPI = ProfileAPI();
Future createProfile({
required BuildContext context,
required ProfileDTO profileDTO,
}) async {
try {
await _profileAPI.createProfile(profileDTO);
} catch (e) {
print(e.toString());
}
}
}
And the API call that sends data to node backend is
class ProfileAPI {
final client = http.Client();
final headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
"Access-Control-Allow-Origin": "*",
};
// Create new Profile
Future createProfile(ProfileDTO profileDTO) async {
final String subUrl = "/profile/create/${profileDTO.useremail}";
final Uri uri = Uri.parse(APIRoutes.BaseURL + subUrl);
try {
final http.Response response = await client.post(uri,
body: jsonEncode(profileDTO.toJson()), headers: headers);
final statusCode = response.statusCode;
final body = response.body;
if (statusCode == 200) {
return body;
}
} catch (e) {
print(e.toString());
}
}
}
EDIT 2
On changing whenComplete to then the linter shows this error.
The argument type 'Future<Object?> Function()' can't be assigned to the parameter type 'FutureOr<dynamic> Function(dynamic)'. dart(argument_type_not_assignable)
What to do? Please help
So from what is see is you are using the whencomplete, but the reason that's happening is the when complete will run every time either its and failure or success. So what i think is you should be using the then Method instead on whencomplete
which will only run in success condition.

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.