State does not update until button is pressed twice - flutter

I am trying to learn and work with APIs, I am using the Tiingo Stock API to get stock info. My current app is:
class _StockAppState extends State<StockApp> {
String out = "Enter Ticker and press Submit";
Map<String,String> headers = {
'Content-Type': 'application/json',
'Authorization' : <API KEY REMOVED>
};
void getPrice(String tick) async {
if(tick == ""){
out = "Enter Ticker and press Submit";
}else{
Response rep = await get('https://api.tiingo.com/tiingo/daily/$tick/prices', headers: headers);
if(rep.statusCode == 200){
List data = json.decode(rep.body);
Map dataS = data[0];
out = "Price: ${dataS['close']}";
}else{
out = "Error";
}
}
}
#override
void initState() {
super.initState();
}
TextEditingController ticker = new TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Stock App'),),
body: Column(
children: <Widget>[
TextField(
controller: ticker,
textAlign: TextAlign.left,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter Ticker',
hintStyle: TextStyle(color: Colors.grey),
),
),
FlatButton(
onPressed: () async {
FocusScope.of(context).unfocus();
setState(() {
getPrice(ticker.text);
});
},
child: Text('Submit')
),
Text(out),
],
),
);
}
}
So, basically, when you enter a ticker and press submit, the app will change the "out" string var to display the stock price. But for the app to update, I am having to press submit twice.
Could anyone help?
P.S.: I have removed my API key for security reasons.

It is because you have async method in your setState method.
The setState method will be called synchronously.
So here problem is when setState is performed and frame get refreshed your data from api has not arrived yet and it showing you the old data. When you again click the button your out variable have new data (from your first click) which will be shown on the screen and API will be called again.
To solve your problem
FlatButton(
onPressed: () async {
FocusScope.of(context).unfocus();
await getPrice(ticker.text);
setState(() {
});
},
child: Text('Submit')
),
So call the setState method after API call is completed.
To know more about async/await watch this video : https://www.youtube.com/watch?v=SmTCmDMi4BY

Related

Data from setstate not accessible Flutter even though its there

I have a textAheadField and successfully get data from it, i call setState so the data can be saved locally in statefullwidget. and i want to store it in database firestore but inside the update method firestore the variable that i want (imageuniversitycollege) is empty and has not been update like in the setstate should be.
This is the textAheadField
String imageuniversitycollege = "";
Widget CollegeBuildTextAhead() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: TypeAheadField<SchoolData?>(
hideSuggestionsOnKeyboardHide: true,
debounceDuration: Duration(milliseconds: 500),
textFieldConfiguration: TextFieldConfiguration(
controller: collegeAheadController,
decoration: InputDecoration(
prefixIcon: Icon(Icons.school),
border: OutlineInputBorder(),
hintText: 'Search your school',
),
),
suggestionsCallback: SchoolApi.getUserSuggestions,
itemBuilder: (context, SchoolData? suggestion) {
final datasugestion = suggestion!;
return ListTile(
leading: Container(
width: 40,
height: 40,
child: Image.network(
datasugestion.image,
fit: BoxFit.cover,
),
),
// leading for image
title: Text(datasugestion.university),
);
},
onSuggestionSelected: (SchoolData? choose) {
final datasugestion = choose!;
collegeAheadController.text = datasugestion.university; //this works fine in the update
final String imageuniversitycollege = datasugestion.image;
setState(() {
final String imageuniversitycollege = datasugestion.image;// this is the data i want
print(imageuniversitycollege); //i print it in here its get the usual link of image
});
},
),
);
}
The usual elevated button
Center(
child: ElevatedButton(
child: const Text(
"Confirm",
),
onPressed: () {
updateEducationCollege();
},
),
)
this is the method update, it works but the image is not filled
Future updateEducationCollege() async {
try {
print(FirebaseAuth.instance.currentUser?.uid);
await FirebaseFirestore.instance
.collection("education")
.doc(FirebaseAuth.instance.currentUser!.uid)
.set({
"uid": FirebaseAuth.instance.currentUser?.uid,
"College": collegeAheadController.text,
"imageCollege": imageuniversitycollege,
}).then((value) => print("Data changed successfully"));
} on FirebaseException catch (e) {
Utils.showSnackBar(e.message);
}
}
The collegeAheadController.text seems fine still successfully retrieve the data like the image bellow
what should i do? to get this data??
Just change
setState(() {
final String imageuniversitycollege = datasugestion.image;
});
to
setState(() {
imageuniversitycollege = datasugestion.image;
});
Instead of updating the existing state variable, you are creating a new local variable. Thats the issue.
Happy coding:)
When you try update your variable you are define new one, change your onSuggestionSelected to this:
onSuggestionSelected: (SchoolData? choose) {
final datasugestion = choose!;
collegeAheadController.text = datasugestion.university;
final String imageuniversitycollege = datasugestion.image;
setState(() {
imageuniversitycollege = datasugestion.image; //<-- change this
print(imageuniversitycollege);
});
},

How to place a Loader on the screen while an API action is being performed in Flutter

I am trying to show a loader when a form is submitted to the server so that there isn't another submission of the same form until and unless the API sends back a response. I have tried something like the below code but it just doesn't seem to work as the Circular Progress indicator seems to not show up and rather, the screen remains as it is until the server sends back a response. As a result of this, the user gets confused as to whether or not their requests got submitted, and in the process, they end up posting the same form another time only to find out later that their were multiple submissions. I will include snippets of the code that has the CircularProgressIndicator() to prevent another submission and the widget that has the API call code.
bool isSelected = false;
isSelected
? const CircularProgressIndicator() : Container(
child: Center(
child: AppButtonStyle(
label: 'Submit',
onPressed: () {
if (_key.currentState!.validate()) { //This is the key of the Form that gets submitted
setState(() {
isSelected = true;
});
List<String> date = [
dateFormat.format(_dateTimeStart!).toString(),
dateFormat.format(_dateTimeEnd!).toString()
];
Map<String, dynamic> data = {
'leave_type': _selectedItem,
'dates': date,
'description': add
};
if (kDebugMode) {
print('DATA: $data');
}
Provider.of<LeaveViewModel>(context, listen: false)
.postLeaveRequests(data, context) //This here makes the API call
.then((value) {
setState(() {
isSelected = false;
_textController.clear();
_dateTimeStart = null;
_dateTimeEnd = null;
});
});
}
},
),
),
)
The API module:
class LeaveViewModel with ChangeNotifier {
final leaveRepository = LeaveRequestRepository();
Future<void> postLeaveRequests(dynamic data, BuildContext context) async {
SharedPreferences localStorage = await SharedPreferences.getInstance();
String authToken = localStorage.getString('token').toString();
leaveRepository.requestLeave(authToken, data).then((value) {
print('LEAVEEEEEE: $value');
Flushbar(
duration: const Duration(seconds: 4),
flushbarPosition: FlushbarPosition.BOTTOM,
borderRadius: BorderRadius.circular(10),
icon: const Icon(Icons.error, color: Colors.white),
// margin: const EdgeInsets.fromLTRB(100, 10, 100, 0),
title: 'Leave Request Submitted',
message: value.data.toString()
).show(context);
}).onError((error, stackTrace) {
Flushbar(
duration: const Duration(seconds: 4),
flushbarPosition: FlushbarPosition.BOTTOM,
borderRadius: BorderRadius.circular(10),
icon: const Icon(Icons.error, color: Colors.white),
// margin: const EdgeInsets.fromLTRB(100, 10, 100, 0),
title: 'Leave Request Failed',
message: error.toString()
).show(context);
});
}
}
Any help will be appreciated. Also, I'm open to the concept of using easy_loader 2.0.0 instead of CicularProgressIndicator() and would be very glad to read suggestions about it's usage in my code.
One problem in your code seems to be that you define isSelected in your build method. Every time you call setState, the build method is called to regenerate the widgets. And with each new call isSelected gets false as initial value. Define isSelected as class variable, so that it is not always on false.
The more elegant solution would be to work with a FutureBuilder
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Flutter Button statemanagement using provider

In my flutter application I have a button for follow and unfollow a user.
buttonstate.isSelected && widget.user!.isFollowing == true
? ElevatedButton(
onPressed: () async {
await unfollowuser(widget.user!.id); //Api Call
},
child: Text(
'Following',
style: TextStyle(
color: Theme.of(context).disabledColor,
),
),
)
: ElevevatedButton(
onPressed: () async {
buttonstate.setButtonState(true);
await followuser(widget.user!.id); //Api Call
},
child: Text(
'Follow',
style: TextStyle(
color: Theme.of(context).accentColor,
),
),
),
My objective is whenever the button is pressed I want the Api call to happen as well as the state of the button should change from 'Follow' to 'Following'. Now the Api gets called, but the button wont change to 'following'. After the page is refreshed, button changes to 'following'. How can i manage state of the button using provider??
Ive created a provider for this purpose as follows
import 'package:flutter/material.dart';
class ButtonProvider with ChangeNotifier {
bool isSelected = false;
void setButtonState(bool value) {
isSelected = value;
notifyListeners();
}
bool? get buttondata {
return isSelected;
}
}

Widget Test Doesn't Fire DropdownButton onChanged

I have a widget test that taps an item in the DropdownButton. That should fire the onChanged callback but it doesn't. Here is the test code. The mock is Mockito.
void main() {
//Use a dummy instead of the fake. The fake does too much stuff
final mockServiceClient = MockTheServiceClient();
final apiClient = GrpcApiClient(client: mockServiceClient);
when(mockServiceClient.logEvent(any))
.thenAnswer((_) => MockResponseFuture(LogEventResponse()));
testWidgets("select asset type", (tester) async {
//sets the screen size
tester.binding.window.physicalSizeTestValue = const Size(3840, 2160);
// resets the screen to its orinal size after the test end
addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
await tester.pumpWidget(AssetApp(apiClient), const Duration(seconds: 5));
//Construct key with '{DDLKey}_{Id}'
await tester
.tap(find.byKey(ValueKey("${assetTypeDropDownKey.value}_PUMP")));
await tester.pumpAndSettle(const Duration(seconds: 5));
verify(mockServiceClient.logEvent(any)).called(1);
});
}
This is the build method of the widget:
#override
Widget build(BuildContext context) {
return DropdownButton<DropDownItemDefinition>(
underline: Container(),
dropdownColor: Theme.of(context).cardColor,
hint: Text(
hintText,
style: Theme.of(context).textTheme.button,
),
//TODO: Use the theme here
icon: Icon(
Icons.arrow_drop_down,
color: Theme.of(context).dividerColor,
),
value: getValue(),
onChanged: (ddd) {
setState(() {
onValueChanged(ddd!);
});
},
items: itemss.map<DropdownMenuItem<DropDownItemDefinition>>((value) {
return DropdownMenuItem<DropDownItemDefinition>(
key: ValueKey(
"${(key is ValueKey) ? (key as ValueKey?)?.value.toString() :
''}_${value.id}"),
value: value,
child: Tooltip(
message: value.toolTipText,
child: Container(
margin: dropdownPadding,
child: Text(value.displayText,
style: Theme.of(context).textTheme.headline3))),
);
}).toList(),
);
}
Note that the onValueChanged function calls the logEvent call. The onChanged callback never happens and the test fails. This is the code it should fire.
Future onAssetTypeChange(DropDownItemDefinition newValue) async {
await assetApiClient.logChange(record.id, newValue, DateTime.now());
}
Why does the callback never fire?
Note: I made another widget test and the Mock does verify that the client was called correctly. I think there is some issue with the callback as part of the widget test.
You need to first instruct the driver to tap on the DropdownButton itself, and then, after the dropdown popup shows up, tap on the DropdownMenuItem.
The driver can't find a DropdownMenuItem from the dropdown if the dropdown itself is not active/painted on the screen.

sms_autofill Unable to detect the OTP

I have used the sms_autofill plugin to to auto-detect the OTP received on mobile and type it in the given PinFieldAutoFill, As per https://pub.dev/packages/sms_autofill#-readme-tab- I am using await SmsAutoFill().listenForCode; function before making my API request & when the request has got a 200 status code I'm navigating it to another page and in init I'm listening to the OTP SMS received by using await SmsAutoFill().listenForCode; I thought like maybe when the page is initialized it wont be listening again to any SMS so i did add a Timer also but still it was not receiving anything.
Dependency - sms_autofill: ^1.2.6
Code : Page1
Center(
child: RaisedButton(
child: Text('Register'),
onPressed: () async {
await SmsAutoFill().listenForCode;
final signCode = await SmsAutoFill().getAppSignature;
print(signCode);
//Http request code
},
),
),
Code : Page2
String appSignature;
String otpCode;
Timer timer;
#override
void codeUpdated() {
setState(() {
otpCode = code;
});
}
#override
void initState() {
super.initState();
listenOTP();
// timer = Timer.periodic(Duration(seconds: 10), (Timer t){
// print('OTP Listening');
// listenOTP();
// });
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Code Received: $otpCode",
),
Container(
padding: EdgeInsets.symmetric(horizontal: 50),
child: PinFieldAutoFill(
codeLength: 6,
onCodeChanged: (val) {
print(val);
},
),
)
],
),
),
);
}
void listenOTP() async {
print('listen for code');
await SmsAutoFill().listenForCode;
}
Maybe your sms format is not right. The sms format should be like:
ExampleApp: Your code is 123456
FA+9qCX9VSu //this is your app signature
if your message is different. You can follow this answer to handle any type of sms format otp https://stackoverflow.com/a/70076071/6067774