Flutter - onStepContinue called automatically on build - flutter

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.

Related

How to add json to an autocomplete widget in flutter

Im trying to pass the data from an API to a list so that I can view it in the Autocomplete widget but I keep getting errors. I tried below code.
This is the code I have tried which passes data to the autocomplete as instance of 'Bus'
GetBuilder<BusesListController>(
init: BusesListController(),
builder: (_) {
_.viewPartners();
return DottedBorder(
child: Padding(
padding:
const EdgeInsets.only(left: 8.0),
child: Autocomplete<Bus>(
optionsBuilder: (TextEditingValue
textEditingValue) {
List<Bus> partnercos = [];
partnercos = _.partners.value as List<Bus>;
// (_.partners).map((value) => Bus.fromJson(value as Map<String, dynamic>)).toList();
print(partnercos);
return partnercos
.where((bus) => bus.company!
.toLowerCase()
.contains(textEditingValue
.text
.toLowerCase()))
.toList();
},
)),
);
}),
I also tried passing _.partners directly but it doesn't work either
Other fix I tried is passing _.partners instead of _.partners. Value above which invokes errors in arena.dart in void _tryToResolveArena which shows that state. Members.length == 1 hence scheduleMicrotask(() => _resolveByDefault(pointer, state));
Contoller code
class BusesListController extends GetxController {
var partners = [].obs;
var isLoaded = false.obs;
final loginController = Get.put(LoginController());
Future<void> viewPartners() async {
final token = loginController.rxToken.value;
var headers = {
'Authorization': 'Bearer $token'
};
try {
var url =
Uri.parse(ApiEndPoints.baseUrl + ApiEndPoints.endpoints.listBusAdmin);
http.Response response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
partners. Value =
(json as List).map((json) => Bus.fromJson(json)).toList();
isLoaded.value = true;
} else {
throw jsonDecode(response.body)["Message"] ?? "Unknown Error Occured";
}
} catch (error) {
// Get.snackbar('Error', error.toString());
}
}
#override
void onInit() {
super.onInit();
viewPartners();
}
}
I am able to print the response so I know the api works but I'm having problems with passing partners list into the autocomplete

Do not use BuildContexts across async gaps lint error on presenter layer

Stateless Widget
CustomButton.build(
label: 'login',
onPressed: () {
presenter.login(context,username,password);
},
);
Presenter class (where we have all the logic)
class Presenter {
Future<void> login(BuildContext context,String username, String password) async {
showDialog(context);
var result = await authenticate(username,password);
int type = await getUserType(result);
Navigator.pop(context); // to hide progress dialog
if(type == 1){
Navigator.pushReplacementNamed(context, 'a');
}else if(type == 2){
Navigator.pushReplacementNamed(context, 'b');
}
}
Future<int> getUserType(User user) async {
//.. some await function
return type;
}
}
Now we are getting Do not use BuildContexts across async gaps. lint error on our presenter wile hiding the dialog and screen navigation.
What is the best way to fix this lint.
Don't stock context directly into custom classes, and don't use context after async if you're not sure your widget is mounted yet.
I see the better practice to fix it is to set an onSuccess method as parameter which will have the Navigator.pop(context); from your UI, but call it inside your main method:
Future<void> login(BuildContext context,String username, String password, void Function() onSuccess) async {
showDialog(context);
var result = await authenticate(username,password);
int type = await getUserType(result);
//Navigator.pop(context); replace this with :
onSuccess.call();
if(type == 1){
Navigator.pushReplacementNamed(context, 'a');
}else if(type == 2){
Navigator.pushReplacementNamed(context, 'b');
}}
after an async gap, there is a possibility that your widget is no longer mounted. thats why context is not recommended to use across an async gap. But if you are so sure that your widget is still mounted then, according to flutter team which you can check here, the recommended approach is to store the context before the await keyword. In your case, this is how it should be done:
Future<void> login(BuildContext context,String username, String password) async {
final nav = Navigator.of(context);
showDialog(context);
var result = await authenticate(username,password);
int type = await getUserType(result);
nav.pop(); // to hide progress dialog
if(type == 1){
nav.pushReplacementNamed('a');
}else if(type == 2){
nav.pushReplacementNamed('b');
}
}

Flutter Refresh List From API

i have a GET function in my flutter code and everytime i add a new item to the list. the list doesn't refresh and won't display the newly added item unless i refresh the whole page.
this is my POST method :
Future<http.Response> ajoutFournisseur(
String numeroFournisseur,
String addressFournisseur,
String matriculeFiscaleFournisseur,
String raisonSocialeFournisseur,
String paysFournisseur,
String villeFournisseur,
double timberFiscaleFournisseur) async {
List fournisseurs = [];
final response = await http.post(
Uri.parse('http://127.0.0.1:8000/api/fournisseur/'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, dynamic>{
'tel': numeroFournisseur,
'adresse': addressFournisseur,
'mf': matriculeFiscaleFournisseur,
'raisonSociale': raisonSocialeFournisseur,
'pays': paysFournisseur,
'ville': villeFournisseur,
'timberFiscale': timberFiscaleFournisseur,
}),
);
if (response.statusCode == 200) {
return fournisseurs = jsonDecode(response.body);
} else {
throw Exception('Erreur base de données!');
}
}
Future<dynamic> future;
and this is code of the button to confirm :
ElevatedButton(
onPressed: (() {
if (_formKey.currentState.validate()) {
// If the form is valid, display a snackbar. In the real world,
// you'd often call a server or save the information in a database.
setState(() {
future = ajoutFournisseur(
numeroFournisseur.text,
addressFournisseur.text,
matriculeFiscaleFournisseur.text,
raisonSocialeFournisseur.text,
paysFournisseur.text,
villeFournisseur.text,
double.parse(timberFiscaleFournisseur.text));
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ajout en cours')),
);
}
}), ...
and this is my GET methos to fetch the items from the list :
fetchFournisseurs() async {
final response =
await http.get(Uri.parse('http://127.0.0.1:8000/api/fournisseur'));
if (response.statusCode == 200) {
var items = jsonDecode(response.body);
setState(() {
fournisseurs = items;
print(fournisseurs[0]['raisonSociale']);
});
} else {
throw Exception('Error!');
}
}
.
.
.
for (var i = 0; i < fournisseurs.length; i++)
Card(
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
ListTile(
title: Text(fournisseurs[i]['raisonSociale']), ...
how can i refresh the list everytime i add a new item without refreshing the whole page ?
I think you first of all need to learn some Flutter good practices.
For example, don't put your logic into the ElevatedButton, set it into a separate Widget function like below :
class Test extends StatefulWidget {
const Test({ Key? key }) : super(key: key);
#override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
Future<void> _handleAjout() async {
if (_formKey.currentState.validate()) {
// First, check your request succeed
try {
var fournisseurs = await ajoutFournisseur(
numeroFournisseur.text,
addressFournisseur.text,
matriculeFiscaleFournisseur.text,
raisonSocialeFournisseur.text,
paysFournisseur.text,
villeFournisseur.text,
double.parse(timberFiscaleFournisseur.text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ajout en cours')),
);
// Will only update state if no error occured
setState(() => future = fournisseurs);
}
on Exception catch(e) {
// Always make sure the request went well
print("error");
}
}
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: (() {
_handleAjout();
}),
)
}
}
And by awaiting POST result, then you can tell your Widget to fetch data and refresh the list by making :
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: (() {
_handleAjout().then(() => fetchFournisseurs());
}),
)
}
The .then function tells Flutter to execute the code contained in the () => myCallbackFunction() only if the previous asynchronous function went well.
By the way, you should always check if your content looks like what you expected before calling setState and set data to your variables :)

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

Returning the result of a Future in a synchronous call

I'm using the signature pad in the FlutterFormBuilder package to capture a signature (FlutterFormBuilderSignaturePad), upload it to firebase storage and then return the download url to the application for storage in a document in firestore.
The problem im facing is that the upload takes a couple of seconds to complete (possibly longer on poor connection). I'm trying to await the call so i can pass the download url to the database however its ignoring my attempts.
Ive tried :
Chaining my calls using the .then() and .whenComplete() but valueTransformer still returns a blank string.
added async to the "valueTransformer", "onSaved" and "onChange" methods and awaited the calls
moved the logic to save the signature between the three methods above in order to give the uimage time to upload
onChanges fires a lot so i introduced a _processing flag so it didnt save the image multiple times and cause database timeouts. onChange was returning a url given a few seconds however i couldn't guarantee the signature was complete.
So my widget looking like this:
final SignatureController _controller = SignatureController(
penStrokeWidth: 5,
penColor: Colors.red,
exportBackgroundColor: Colors.blue,
);
String _signature;
File _signatureFile;
bool _processing;
return FormBuilderSignaturePad(
name: 'signature',
controller: _controller,
decoration: InputDecoration(labelText: "signature"),
initialValue: _signatureFile?.readAsBytesSync(),
onSaved: (newValue) async {
//called on save just before valueTransformer
await processSignature(newValue, context);
},
valueTransformer: (value) {
//called when the form is saved
return _signature;
},
onChanged: (value) {
//called frequently as the signature changes
if (_controller.isNotEmpty) {
if (_controller.value.length > 19) {
if (!_processing) {
processSignature(value, context).then((value) {
setState(() {
_processing = false;
});
});
}
}
}
},
)
My future for processing the upload and setting the state
Future<void> processSignature(dynamic signature, BuildContext context) async {
setState(() {
_processing = true;
});
var bytes = await _controller.toPngBytes();
final documentDirectory = await getApplicationDocumentsDirectory();
final file =
File(join(documentDirectory.path, 'signature${database.uid}.png'));
file.writeAsBytesSync(bytes);
var url = await storage.uploadImage(
context: context,
imageToUpload: file,
title: "signature${database.uid}.png",
requestId: database.currentRequest.id);
setState(() {
_signature = url.imageUrl;
_signatureFile = file;
});
}
UPDATES AFTER CHANGES BELOW
Process Signature:
Future<String> processSignature(
dynamic signature, BuildContext context) async {
var bytes = await _controller.toPngBytes();
final documentDirectory = await getApplicationDocumentsDirectory();
final file =
File(join(documentDirectory.path, 'signature${database.uid}.png'));
file.writeAsBytesSync(bytes);
var url = await storage.uploadImage(
context: context,
imageToUpload: file,
title: "signature${database.uid}.png",
requestId: database.currentRequest.id);
return url.imageUrl;
}
Signature Pad Widget:
return FormBuilderSignaturePad(
name: 'signature',
controller: _controller,
decoration: InputDecoration(labelText: "signature"),
initialValue: _signatureFile?.readAsBytesSync(),
onSaved: (newValue) async {},
valueTransformer: (value) async {
final savedUrl = await processSignature(value, context);
return savedUrl;
},
onChanged: (value) {},
);
Method where im seeing the "Future"
_formKey[_currentStep].currentState.save();
if (_formKey[_currentStep].currentState.validate()) {
//request from the database
var request = firestoreDatabase.currentRequest;
//this should be the url however its returning as
//"Future<String>"
var value = _formKey[_currentStep].currentState.value;
request.questions[_currentStep].result =
jsonEncode(_formKey[_currentStep].currentState.value);
request.questions[_currentStep].completedOn =
Timestamp.fromDate(new DateTime.now());
firestoreDatabase.updateRequest(request).then((value) {
if (_currentStep == _totalSteps - 1) {
//pop the screen
Navigator.pop(context);
} else {
setState(() {
_currentStep++;
});
}
It impossible to return async result in sync call. Future means it completes somewhere in future.
Remove processSignature from onChanged (why send signature each time it modified?) and process it in onSaved. Then you can use async/await to send signature to server and wait for result url.
class _SomeWidgetState extends State<SomeWidget> {
/// Form key
final formKey = GlobalKey<FormState>();
/// Contains signature binary daya
Uint8List signatureValue;
#override
void build(...) {
return Column(
children: [
FormBuilderSignaturePad(
...
onSaved(Uint8List value) async {
signatureValue = value;
},
FlatButton(
child: Text('Submit'),
onPressed: () {
_submit();
}
),
],
);
}
/// Submits form
Future< void> _submit() async {
if (formKey.currentState.validate()) {
formKey.currentState.save(); // calls all `onSaved` for each form widgets
// So at this point you have initialized `signatureValue`
try {
final signatureUrl = await processSignature(signatureValue, context); // save into database
await doSomethingWithUrl(signatureUrl); // insert into document
} on SomeExceptionIfRequired catch (e) {
// Show error if occurred
ScaffoldMessenger.of(context).showSnackbar(...);
}
}
}
}