I use listen() in retrieving data from Firestore. I am also using setData and merge: true to update the data in Firestore. When there is an update in the Firestore it will automatically refresh the whole page. How can I stop that from doing so. I want it to change the value but not refresh the page. Is it possible? Any documentation or sample code is welcome. Thank you in advance.
Update
This is my DropDownButton:
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DropdownButton(
items: routeName.map((value) {
return DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
underline: SizedBox(height: 0,),
onChanged: (value) => setState((){
selectedItem = value;
polylineCoordinates.clear();
getRoute();
}),
hint: new Text ("Select a Route"),
value: selectedItem,
)
],
),),
This is where I add in the value in the DropDownButton:
List <String> routeName = [];
Future <void> getRouteName() async{
Firestore.instance.collection('routes').snapshots().listen((RouteData) async {
routeName.clear();
if(RouteData.documents.isNotEmpty){
for (int i = 0; i < RouteData.documents.length; i++){
if(i == 0){
routeName.add("No Route");
}
routeName.add(RouteData.documents[i].documentID);
}
}
setState(() {
});
});
}
This is my initState()
#override
void initState(){
geoService.getCurrentLocation().listen((position){
centerScreen(position);
});
geoService.getCurrentLocation().listen((position) {
_addGeoPoint(position);
});
getMarker();
getRouteName();
retrieveRoute();
super.initState();
}
This is where things go wrong:
Future <void> getRoute() async{
if (selectedItem == "No Route"){
var user = await FirebaseAuth.instance.currentUser();
Firestore.instance.collection('user').document(user.email).setData(
{"Route": "No Route"}, merge: true);
}
var user = await FirebaseAuth.instance.currentUser();
Firestore.instance.collection('user').document(user.email).setData(
{"Route": selectedItem}, merge: true);
}
Because I want it to update to the Firestore whenever I update the value in the DropDownButton, but after the update it will refresh my page and I don't want it to refresh the whole page. I just want it to save to Firestore and update at the DropDownButton only.
If you want to listen to a Stream just one time, you may use the first operator on it:
Firestore.instance.collection('routes').snapshots().first.then(() => {
// this will be called juste once
});
The problem comes from your getRouteName method.
Future <void> getRouteName() async{
Firestore.instance.collection('routes').snapshots().listen((RouteData) async {
routeName.clear();
if(RouteData.documents.isNotEmpty){
for (int i = 0; i < RouteData.documents.length; i++){
if(i == 0){
routeName.add("No Route");
}
routeName.add(RouteData.documents[i].documentID);
}
}
setState(() {
});
});
}
}
Every time you call setState, it will cause the UI to be repainted. Since you use listen to get called for any changed to routes, and call setState() for every such change, any change to routes will repaint the UI.
If you don't want to repaint the UI altogether, remove the call to setState(). If you want to repaint the UI only in certain conditions, wrap the call to setState() in those conditions.
Related
I have got a State Management Problem I couldn't get rid of and I want to reach out to you.
Basically, I activate with the Buttons a game and I am sending a String to the uC. The uC does its stuff and sends a response to Flutter including gameFinished=true (that works).
Now I want to reset the State of the Button to the init state WITHOUT pressing the Button. Following are some things I tried that didn't work.
#override
void initState() {
super.initState();
setState(() {
gameAktivated = false;
gameStarted = false;
});
}
void asyncSetState() async {
setState(() async {
gameAktivated = false;
gameStarted = false;
});
}
I am changing the style from "Start" to "Stop" when the Button is pressed and I send Data to the uC. (Works)
Edit: Ofc I have a second button that triggers gameAktivated=true :)
ElevatedButton(
onPressed: () {
if (gameAktivated) {
setState(() {
gameStarted = !gameStarted;
});
if (gameStarted) {
//Send Data to uC
} else if (!gameStarted) {
//Send Data to uC
}
}
},
child:
!gameStarted ? const Text('Start') : const Text('Stop'),
),
Button Displays Stop now.
Following I am receiving a String from the uC that I jsonEncode and I receive gameFinished=true. (Works)
Container(
child: streamInit
? StreamBuilder<List<int>>(
stream: stream,
builder: (BuildContext context,
AsyncSnapshot<List<int>> snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState ==ConnectionState.active) {
// getting data from Bluetooth
var currentValue =const BluetoothConnection().dataParser(snapshot.data);
config.jsonDeserializeGameFinished(currentValue);
if(config.gameFinished){
setState(() {
gameAktivated = false;
gameStarted = false;
});
asyncSetState();//Tested both methods seperate!
}
return Column(
children: [
Text(config.time.toString()),
],
);
} else {
return const Text(
'Check the stream',
textAlign: TextAlign.center,
);
}
},
): const Text("NaN",textAlign: TextAlign.center,),
),
When I try to reset the state like in the code above this error occures:
Calling setState Async didnt work for me either.
Where and how can I set the state based on the response from the uC?
Is it possible without using Provider Lib?
Thanks in advance Manuel.
Actually this error is not about the changing the state of button. Its a common mistake to update the widget state when its still building the widget tree.
Inside your StreamBuilder, you are trying to update the state before creating the UI which is raising this issue.
if(config.gameFinished){
setState(() {
gameAktivated = false;
gameStarted = false;
});
This will interrupt the build process of StreamBuilder as it will start updating the whole page. You need to move it out of the StreamBuilder's builder method.
To do that simply convert your stream to a broadcast, which will allow you to listen your stream multiple time.
var controller = StreamController<String>.broadcast();
Then inside the initState of the page you can setup a listener method to listen the changes like this
stream.listen((val) => setState((){
number = val;
}));
Here you can change the state values because from here it will not interrupt the widget tree building cycle.
For more details see this example I created
https://dartpad.dev/?id=a7986c44180ef0cb6555405ec25b482d
If you want to call setState() immediately after the build method was called you should use:
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// this method gets called once directly after the previous setState() finishes.
});
Answer to my own Question:
In initState()
added this:
stream.listen((event) {
String valueJSON = const BluetoothConnection().dataParser(event);
config.jsonDeserializeGameFinished(valueJSON);
if (config.gameFinished) {
setState(() {
gameAktivated = false;
gameStarted = false;
});
}
});
The Code above listens to the stream, UTF-8 Decodes and JSON-Decodes the data. After this you can access the variable to set a state.
Following on from Unhandled Exception: type '_DropdownRouteResult<int>' is not a subtype of type 'int?' when returning value from Navigator.pop() as I still haven't resolved the issue.
I have a DropdownFormField which I am dynamically populating from a db via a Provider. I would like to add a DropdownMenuItem which, when selected, pushes a new route (for inserting a new record into the db).
The route returns the id of the newly-inserted db record when popped, and I would like to set the new value as the value of the DropdownFormField.
Implementing the new item with a TextButton child and pushing in the buttons' onPressed results in expected push/pop behaviour, but is styled inconsistently from the "normal" items, and does not close the dropdown (which makes sense as the button is pressed, but the DropdownMenuItem is not tapped). Tapping outside the dropdown after popping reveals that the dropdown's value is updated correctly.
DropdownMenuItem<int>(child: TextButton(
onPressed: () async {
final int newValue = await Navigator.push(context, AddNewTeaProducerRoute());
setState(() {
_selectedValue = newValue;
});
},
child: Text('Add New Manufacturer')));
Implementing the new item with a Text child and pushing in the DropdownMenuItem's onTap (which seems like the correct approach) results in an immediate attempt to return the value, disrespecting the asynchronous nature of the onTap and resulting in the exception from my previous question. Breakpoint debugging without specifying the type of newValue shows that it is immediately assigned the Future/_DropdownRouteResult<int>, rather than awaiting its returned int.
DropdownMenuItem<int>(
onTap: () async {
final int newValue = await Navigator.push(context, AddNewTeaProducerRoute());
setState(() {
_selectedValue = newValue;
});
},
child: const Text('Add New Manufacturer'));
I have no idea why await is being respected in TextButton.onPressed but not in DropdownMenuItem.onTap
I don't know if it's the right way, since it relies on null as a placeholder value and I can't see how you'd easily scale it beyond a single DropdownMenuItem with special behaviour (as unlikely as it seems that you'd want to) but after reading this for the third time I finally grokked a solution - return null as the value, and perform navigation/assignment in the DropdownButtonFormField's onChanged
final brokenAddNewTeaProducerButton = DropdownMenuItem<int>(
value: null,
child: const Text('Add New Manufacturer'));
return DropdownButtonFormField<int?>(
value: _selectedValue,
items: [brokenAddNewTeaProducerButton] + teaProducerListItems,
onChanged: (value) async {
if (value == null) {
final newTeaProducerId = await Navigator.push(context, AddNewTeaProducerRoute());
setState(() {
_selectedValue = newTeaProducerId;
});
} else {
setState(() {
_selectedValue = value;
});
}
},
hint: Text('Select a manufacturer'),
);
}
**You can try this statfulBuilder**
StatefulBuilder(builder: (context,state){
return DropdownMenuItem<int>(child: TextButton(
onPressed: () async {
var newValue = await Navigator.push(context,
AddNewTeaProducerRoute());
state(() {
_selectedValue = newValue;
});
},
child: Text('Add New Manufacturer')));
}),
I need to get location data which is used to calculate the distance between the user and other locations. This is on the app's home page and I don't want to do this every time the page loads, that's why I set up a timestamp and the location is grabbed only if five minutes pass.
What I have now is something like this in the home page:
LocationData _currentPosition;
#override
void initState() {
super.initState();
_getLocationData();
}
_getLocationData() async {
final content = Provider.of<Content>(context.read());
final _timestamp = DateTime.now().millisecondsSinceEpoch;
final _contentTimestamp = content.homeTimestamp;
if ((_contentTimestamp == null) ||
((_timestamp) - _contentTimestamp) >= 300000) {
try {
_locationData = await location.getLocation();
content.homeTimestamp = _timestamp;
setState(() {
_currentPosition = _locationData;
});
} on Exception catch (exception) {
print(exception);
} catch (error) {
print(error);
}
}
}
And I store the timestamp in Provider because I want it to persist when the user leaves the home page and returns. Not sure how to set it up without notifyListeners()
int _homeTimestamp;
int get homeTimestamp => _homeTimestamp;
set homeTimestamp(int newValue) {
_homeTimestamp = newValue;
notifyListeners();
}
My problem is that sometimes the location doesn't get stored and the page doesn't load. Is there a better way to do this?
I was thinking of adding a FutureBuilder in the body, but that would mean that the location will be retrieved every time the user loads the page, or I can just do the timestamp check in the body and not load the FutureBuilder all the time, but that doesn't seem right.
body: _currentPosition == null
? Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator(),
),
SizedBox(
width: 10.0,
),
Text("Getting your location..."),
],
),
)
: Column(
...
use shared preference to check if the _getLocationData() method is called and use Timer.periodic(); method to call the function every five minutes.
I am looking for an example of how to handle forms and validation in best practice with GetX?
Is there any good example of that or can someone show me an example of how we best can do this?
Here's an example of how you could use GetX's observables to dynamically update form fields & submit button.
I make no claim that this is a best practice. I'm sure there's better ways of accomplishing the same. But it's fun to play around with how GetX can be used to perform validation.
Form + Obx
Two widgets of interest that rebuild based on Observable value changes:
TextFormField
InputDecoration's errorText changes & will rebuild this widget
onChanged: fx.usernameChanged doesn't cause rebuilds. This calls a function in the controller usernameChanged(String val) when form field input changes.
It just updates the username observable with a new value.
Could be written as:
onChanged: (val) => fx.username.value = val
ElevatedButton (a "Submit" button)
onPressed function can change between null and a function
null disables the button (only way to do so in Flutter)
a function here will enable the button
class FormObxPage extends StatelessWidget {
const FormObxPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
FormX fx = Get.put(FormX()); // controller
return Scaffold(
appBar: AppBar(
title: const Text('Form Validation'),
),
body: SafeArea(
child: Container(
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(
() {
print('rebuild TextFormField ${fx.errorText.value}');
return TextFormField(
onChanged: fx.usernameChanged, // controller func
decoration: InputDecoration(
labelText: 'Username',
errorText: fx.errorText.value // obs
)
);
},
),
Obx(
() => ElevatedButton(
child: const Text('Submit'),
onPressed: fx.submitFunc.value, // obs
),
)
],
),
),
),
);
}
}
GetX Controller
Explanation / breakdown below
class FormX extends GetxController {
RxString username = RxString('');
RxnString errorText = RxnString(null);
Rxn<Function()> submitFunc = Rxn<Function()>(null);
#override
void onInit() {
super.onInit();
debounce<String>(username, validations, time: const Duration(milliseconds: 500));
}
void validations(String val) async {
errorText.value = null; // reset validation errors to nothing
submitFunc.value = null; // disable submit while validating
if (val.isNotEmpty) {
if (lengthOK(val) && await available(val)) {
print('All validations passed, enable submit btn...');
submitFunc.value = submitFunction();
errorText.value = null;
}
}
}
bool lengthOK(String val, {int minLen = 5}) {
if (val.length < minLen) {
errorText.value = 'min. 5 chars';
return false;
}
return true;
}
Future<bool> available(String val) async {
print('Query availability of: $val');
await Future.delayed(
const Duration(seconds: 1),
() => print('Available query returned')
);
if (val == "Sylvester") {
errorText.value = 'Name Taken';
return false;
}
return true;
}
void usernameChanged(String val) {
username.value = val;
}
Future<bool> Function() submitFunction() {
return () async {
print('Make database call to create ${username.value} account');
await Future.delayed(const Duration(seconds: 1), () => print('User account created'));
return true;
};
}
}
Observables
Starting with the three observables...
RxString username = RxString('');
RxnString errorText = RxnString(null);
Rxn<Function()> submitFunc = Rxn<Function()>(null);
username will hold whatever was last input into the TextFormField.
errorText is instantiated with null initial value so the username field is not "invalid" to begin with. If not null (even empty string), TextFormField will be rendered red to signify invalid input. When a non-valid input is in the field, we'll show an error message. (min. 5 chars in example:)
submitFunc is an observable for holding a submit button function or null, since functions in Dart are actually objects, this is fine. The null value initial assignment will disable the button.
onInit
The debounce worker calls the validations function 500ms after changes to the username observable end.
validations will receive username.value as its argument.
More on workers.
Validations
Inside validations function we put any types of validation we want to run: minimum length, bad characters, name already taken, names we personally dislike due to childhood bullies, etc.
For added realism, the available() function is async. Commonly this would query a database to check username availability so in this example, there's a fake 1 second delay before returning this validation check.
submitFunction() returns a function which will replace the null value in submitFunc observable when we're satisfied the form has valid inputs and we allow the user to proceed.
A little more realistic, we'd prob. expect some return value from the submit button function, so we could have the button function return a future bool:
Future<bool> Function() submitFunction() {
return () async {
print('Make database call to create ${username.value} account');
await Future.delayed(Duration(seconds: 1), () => print('User account created'));
return true;
};
}
GetX is not the solution for everything but it has some few utility methods which can help you achieve what you want. For example you can use a validator along with SnackBar for final check. Here is a code snippet that might help you understand the basics.
TextFormField(
controller: emailController,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (!GetUtils.isEmail(value))
return "Email is not valid";
else
return null;
},
),
GetUtils has few handy methods for quick validations and you will have to explore each method to see if it fits your need.
Getting the error (title) when I try to assign the return of this method to a widget parameter. The suggestions: is expect a List<dynamic> not a Future<List<dynamic>>. Is FutureBuilder the only way? The AutoCompleteTextField in this widget is a type ahead so will be calling getLocationSuggestionsList every .5 seconds after keystrokes stop (not sure if that matters in answering this question).
#override
Widget build(BuildContext context) {
return Container(
child: new Center(
child: Column(children: <Widget>[
new Column(children: <Widget>[
searchTextField = AutoCompleteTextField<dynamic>(
suggestions: getLocationSuggestionsList("sd"),
style: new TextStyle(color: Colors.black, fontSize: 16.0),
decoration: new InputDecoration(
.....
Future<List<dynamic>> getLocationSuggestionsList(String locationText) async {
List<String> suggestionList = List();
Map suggestionsKeyValuePairs = Map<String, String>();
dynamic data = await GoogleMapsServices.getAddressPrediction(
locationText,
LatLng(currentLocation.latitude, currentLocation.longitude),
);
if (data != null) {
for (dynamic predictions in data.predictions) {
suggestionsKeyValuePairs[predictions.description] = predictions.placeId;
if (!suggestionList.contains(predictions.description))
suggestionList.add(predictions.description);
}
return suggestionList;
} else {
return [''];
}
}
The cause for this error is that the suggestions parameter expects a List not a Future.
What you can do is create a state variable and assign the result of your getLocationSuggestionsList() function to that with a setState() call or any other state management mechanism so that whenever the state changes the UI builds again with the relevant data.
class YourClass extends StatefulWidget {
///
}
class _YourClassState extends State<YourClass>{
/// Your state variable here. Initialize with data that will be showing if actual data not available.
List<dynamic> suggestionList = ["];
initState(){
/// call you get suggestion function on init or any other lifecycle methods as per your need, may be inside build
getLocationSuggestionsList("sd");
super.initState();
}
#override
Widget build(context){
return AutoCompleteTextField<dynamic>(
suggestions: suggestionList,
style: new TextStyle(color: Colors.black, fontSize: 16.0),
decoration: new InputDecoration()
/// ....
);
}
void getLocationSuggestionsList(String locationText) async {
List<String> sList = List();
Map suggestionsKeyValuePairs = Map<String, String>();
dynamic data = await GoogleMapsServices.getAddressPrediction(
locationText,
LatLng(currentLocation.latitude, currentLocation.longitude),
);
if (data != null) {
for (dynamic predictions in data.predictions) {
suggestionsKeyValuePairs[predictions.description] = predictions.placeId;
if (!sList.contains(predictions.description))
sList.add(predictions.description);
}
} else {
sList = [''];
}
setState((){
suggestionList = List;
/// This will render your UI again with updates suggestionList
});
}
}
getLocationSuggestionsList() is async and return a future, if you want to get the result (List<dynamic>), you need to call it with await keyword.
await getLocationSuggestionsList("sd")
But, this is only possible into async functions/methods.
You can resolve this by many ways:
Use FutureBuilder
Do it with reactive programing architecture (Bloc, Rx, raw streams, etc...)
Do it like krishnakumarcn ;) https://stackoverflow.com/a/62187158/13569191