Flutter: Prevent executed feturebuilder when setState is occurred - flutter

I am trying to load DropDownMenu inside Future builder.In my widget i have a Column. Inside Column I have a few widget :
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(),
Divider(),
Container(),
...widget._detailsModel.data.appletActions.map((item) {
.....
...item.appletInputs.map((inputs) {
FutureBuilder(
future: MyToolsProvider()
.getDropDownConfiges(inputs.dataUrl),
builder:
(ctx,AsyncSnapshot<DropDownModel.DropDownConfigToolsModle>snapshot) {
if (!snapshot.hasData ||
snapshot.connectionState ==
ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasData &&
snapshot.connectionState ==
ConnectionState.done) {
_dropDown = snapshot.data.data[0];
return DropdownButton<DropDownModel.DataModle>(
hint: Text("Select Item"),
value: _dropDown,
onChanged: (data) {
setState(() {
_dropDown = data;
});
},
items: snapshot.data.data.map((item) {
return DropdownMenuItem<
DropDownModel.DataModle>(
value: item,
child: Row(
children: <Widget>[
Icon(Icons.title),
SizedBox(
width: 10,
),
Text(
item.title,
style: TextStyle(
color: Colors.black),
),
],
),
);
}).toList(),
);
} else {
return Center(
child: Text('failed to load'),
);
}
}),
}
}
]
As you can see i have FutureBuilder inside a loop to show DropdownButton.everything is ok and code works as a charm but my problem is :
onChanged: (data) {
setState(() {
_dropDown = data;
})
every time setState called, future: MyToolsProvider().getDropDownConfiges(inputs.dataUrl), is executed and
_dropDown = snapshot.data.data[0]; again initialized and it get back in a first time .
It is not possible declared MyToolsProvider().getDropDownConfiges(inputs.dataUrl), in initState() method because inputs.dataUrl it is not accessible there.
How can i fixed that?

Updating parent state from within a builder is anti-pattern here. To reduce future errors and conflicts I recommend to wrap the parts that use and update _dropDown variable as a statefull widget.
Afterward the builder is just responsible of selecting correct widget based on future results and separated widget will only update itself based on interactions. Then hopefully many current and potential errors will disappear.

Do one thing, change this
_dropDown = snapshot.data.data[0];
to
_dropDown ??= snapshot.data.data[0];
What this will do is, it will check if _dropDown is null then assign it with value otherwise it won't.

Related

How to change variable value in flutter with bloc?

Want to ask is How to change variable value with stream flutter?
You think my question is so fundamental and I can search in everywhere on internet. But in this scenario with stream, I can't change the variable value with method. How I need to do? please guide me. I will show with example.
Here, this is bloc class code with rxDart.
class ChangePinBloc {
final ChangePinRepository _changePinRepository = ChangePinRepository();
final _isValidateConfirmNewPinController = PublishSubject();
String oldPin = '';
Stream get isValidateConfirmNewPinStream =>
_isValidateConfirmNewPinController.stream;
void checkValidateConfirmNewPin(
{required String newPinCode, required String oldPinCode}) {
if (newPinCode == oldPinCode) {
oldPin = oldPinCode;
changePin(newCode: newPinCode);
isValidateConfirmPin = true;
_isValidateConfirmNewPinController.sink.add(isValidateConfirmPin);
} else {
isValidateConfirmPin = false;
_isValidateConfirmNewPinController.sink.add(isValidateConfirmPin);
}
}
void changePin({required String newCode}) async {
changePinRequestBody['deviceId'] = oldPin;
}
dispose() {
}
}
Above code, want to change the value of oldPin value by calling checkValidateConfirmNewPin method from UI. And want to use that oldPin value in changePin method. but oldPin value in changePin always get empty string.
This is the calling method checkValidateConfirmNewPin from UI for better understanding.
PinCodeField(
pinLength: 6,
onComplete: (value) {
pinCodeFieldValue = value;
changePinBloc.checkValidateConfirmNewPin(
newPinCode: value,
oldPinCode: widget.currentPinCodeFieldValue!);
},
onChange: () {},
),
Why I always get empty String although assign a value to variable?
Lastly, this is complete code that calling state checkValidateConfirmNewPin from UI.
void main() {
final changePinBloc = ChangePinBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: StreamBuilder(
stream: changePinBloc.isValidateConfirmNewPinStream,
builder: (context, AsyncSnapshot pinValidateSnapshot) {
return Stack(
children: [
Positioned.fill(
child: Column(
children: [
const PinChangeSettingTitle(
title: CONFIRM_NEW_PIN_TITLE,
subTitle: CONFIRM_NEW_PIN_SUBTITLE,
),
const SizedBox(
height: margin50,
),
Padding(
padding: const EdgeInsets.only(
left: margin50, right: margin50),
child: PinCodeField(
pinLength: 6,
onComplete: (value) {
changePinBloc.checkValidateConfirmNewPin(
newPinCode: value,
oldPinCode: widget.newCodePinValue!,
);
},
onChange: () {},
),
)
],
),
),
pinValidateSnapshot.hasData
? pinValidateDataState(pinValidateSnapshot, changePinBloc)
: const Positioned.fill(
child: SizedBox(),
),
],
);
},
),
),
);
}
}
To update the variable you should emit a new state using emit() method.
Just make sure your bloc is correct as it should inherit from Bloc object. Read flutter_bloc documentation to know how to use it.
A simple example:
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
ExampleBloc() : super(ExampleInitial()) {
on<ExampleEvent>((event, emit) {
//Do some logic here
emit(ExampleLoaded());
});
}
}

Can I use Dismissible without actually dismissing the widget?

I'm trying to make a widget that can be swiped to change the currently playing song in a playlist. I'm trying to mimic how other apps do it by letting the user swipe away the current track and the next one coming in. Dismissible is so close to what I actually want. It has a nice animation and I can easily use the onDismissed function to handle the logic. My issue is that Dismissible actually wants to remove the widget from the tree, which I don't want.
The widget I'm swiping gets updated with a StreamBuilder when the song changes, so being able to swipe away the widget to a new one would be perfect. Can I do this or is there a better widget for my needs?
Here's the widget I'm working on:
class NowPlayingBar extends StatelessWidget {
const NowPlayingBar({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<ScreenState>(
stream: _screenStateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
final screenState = snapshot.data;
final queue = screenState.queue;
final mediaItem = screenState.mediaItem;
final state = screenState.playbackState;
final processingState =
state?.processingState ?? AudioProcessingState.none;
final playing = state?.playing ?? false;
if (mediaItem != null) {
return Container(
width: MediaQuery.of(context).size.width,
child: Dismissible(
key: Key("NowPlayingBar"),
onDismissed: (direction) {
switch (direction) {
case DismissDirection.startToEnd:
AudioService.skipToNext();
break;
case DismissDirection.endToStart:
AudioService.skipToPrevious();
break;
default:
throw ("Unsupported swipe direction ${direction.toString()} on NowPlayingBar!");
}
},
child: ListTile(
leading: AlbumImage(itemId: mediaItem.id),
title: mediaItem == null ? null : Text(mediaItem.title),
subtitle: mediaItem == null ? null : Text(mediaItem.album),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (playing)
IconButton(
onPressed: () => AudioService.pause(),
icon: Icon(Icons.pause))
else
IconButton(
onPressed: () => AudioService.play(),
icon: Icon(Icons.play_arrow)),
],
),
),
),
);
} else {
return Container(
width: MediaQuery.of(context).size.width,
child: ListTile(
title: Text("Nothing playing..."),
));
}
} else {
return Container(
width: MediaQuery.of(context).size.width,
// The child below looks pretty stupid but it's actually genius.
// I wanted the NowPlayingBar to stay the same length when it doesn't have data
// but I didn't want to actually use a ListTile to tell the user that.
// I use a ListTile to create a box with the right height, and put whatever I want on top.
// I could just make a container with the length of a ListTile, but that value could change in the future.
child: Stack(
alignment: Alignment.center,
children: [
ListTile(),
Text(
"Nothing Playing...",
style: TextStyle(color: Colors.grey, fontSize: 18),
)
],
));
}
},
);
}
}
Here's the effect that I'm going for (although I want the whole ListTile to get swiped, not just the song name): https://i.imgur.com/ZapzpJS.mp4
This can be done by using the confirmDismiss callback instead of the onDismiss callback. To make sure that the widget never actually gets dismissed, you need to return false at the end of the function.
Dismissible(
confirmDismiss: (direction) {
...
return false;
}
)

Using Flutter Checkbox() and obtaining initial value from Firestore

I'm no experienced programmer and I could find no guidance, hence this question. I have a working solution but not sure if this is good practice.
I am using the widget Checkbox() in a Form(). Widgets like TextFormField() and DateTimeField() have a parameter called 'initialValue'. Checkbox() does not.
For TextFormField() and DateTimeField() I obtained the initialValue by:
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UnitDetails>(
stream: DatabaseServices(uid: user.userUid, unitUid: widget.unitUid)
.unitByDocumentID,
builder: (context, unitDetails) {
if (!unitDetails.hasData) return Loading();
return Scaffold(
etc
The Checkbox(value: residentialUnit,) can not have its initial value set inside the builder:. The parameter 'value:' needs to be set true or false before the builder: ie before the value is obtained from Firestore! The way I solved this is by using initState(). An extra call to Firestore and more code for this one input widget.
#override
void initState() {
super.initState();
Firestore.instance
.collection("units")
.document(widget.unitUid)
.snapshots()
.listen((snapshot) {
residentialUnit = snapshot.data['unitResidential'];
});
}
Is there a better way?
I think you can solve your problem with the following answer (using FormField).
Checkbox form validation
Following is a sample code.
FormField(
initialValue: userProfile.agreement,
builder: (state) {
return Column(
children: [
Row(
children: [
Checkbox(
activeColor: Colors.blue,
value: state.value,
onChanged:(value) {
setState(() {
state.didChange(value);
});
}
),
Expanded(child: Text('Sample checkbox')),
],
),
Text(
state.errorText ?? '',
style: TextStyle(
color: Theme.of(context).errorColor,
),
)
],
);
},
validator: (val) {
print('VAL: $val');
if (!val) {
return 'You need to accept terms';
} else {
return null;
}
},
)

I am getting error while using stream builder in flutter

i am making a mobile app using flutter. And i am using stream builder for this screen. I am not getting the point where i am wrong in the code. Can you please help me in this. I am sharing code and screenshot for this particular row which is causing problem
var timeSelected = 'Click here';
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'Time Slot:',
style: TextStyle(color: Colors.white),
),
Spacer(),
GestureDetector(
onTap: () {
_asyncInputDialog(context);
//_displayDialog();
},
child: StreamBuilder(stream: cartManager.getTimeSlotSelected,
initialData: timeSelected,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData){
timeShow(snapshot,);
}
else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(
child: Container(
child: Text('Select time slot'),
),
);
},)
),
],
),
This alert dialog will show when i click on the text of row:
_asyncInputDialog(
BuildContext context,
) {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Center(child: Text('Available Time Slot')),
content: TEAlertDialogContent(),
actions: <Widget>[
new FlatButton(
child: new Text('CANCEL'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
);
});
}
When i got the value from showdialog i will store the value in streamcontroller that is present in CartManager.
static StreamController<Timeslot> timeSlotController = BehaviorSubject();
timeSlotSelected(Timeslot time){
timeSlotController.sink.add(time);
}
get getTimeSlotSelected{
return timeSlotController.stream;
}
And we call the above method in stream property of streamcontroller and get the snapshot. This is the method which was called when our snapshot has data:
Widget timeShow(AsyncSnapshot<Timeslot> snapshot ) {
timeSelected = '${snapshot.data.firstTimeSlot}-${snapshot.data.secondTimeSlot}';
timeslotid = snapshot.data.id.toString();
return Text(timeSelected);
}
But i am getting error: type 'BehaviorSubject' is not a subtype of type 'Stream'
Please let me know where i am wrong. I had also shared a screen shot of screen showing this error too.
As your error states, you are trying to pass a type Timeslot to a Stream builder expecting a stream of type String. You must check which one is correct (String or Timeslot) and use the same type on both sides.
Apparently, your problem is in the timeSelected variable. Where is it defined? If this is a String, the Stream builder will infer that your stream is of type String, which is not true. You must set this variable as a Timeslot, since this is your stream type.
Also, you have an error in your code. You have to return a widget to be rendered if snapshot has data. Check the code below:
StreamBuilder(stream: cartManager.getTimeSlotSelected,
initialData: timeSelected,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData){
return timeShow(snapshot,);
}
else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(
child: Container(
child: Text('Select time slot'),
),
);
},)

TextField reloads FutureBuilder when pressed/left in Flutter

The user can either enter the answer with InputChips or manually type it in the TextField. When I try with InputChips, the correct answer is not detected. When I try to manually type it, the FutureBuilder reloads when I enter and leave the TextField. What is the reason?
The Future function should only be called once because it fetches a random document from Firestore, splits the String and scrambles the different pieces. It is some form of quiz.
class _buildPhrases extends State<PhrasesSession>{
TextEditingController _c;
String _text = "initial";
#override
void initState(){
_c = new TextEditingController();
super.initState();
}
#override
void dispose(){
_c?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final Arguments args = ModalRoute.of(context).settings.arguments;
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
// TODO: implement build
return Scaffold(
body: Column(
children: <Widget>[
Flexible(flex: 2, child: _buildRest(context),),
Flexible(flex: 5,
child: FutureBuilder(
future: getEverything(args.colName),
builder: (context, snapshot){
if(!snapshot.hasData){
return Center(child: CircularProgressIndicator(),);
}else{
return Column(
children: <Widget>[
Flexible(flex: 1, child: Text(snapshot.data[1]),),
Divider(),
Flexible(flex: 2, child: Container(
child: TextField(
onChanged: (t){
_text += "$t ";
if(_c.text == snapshot.data[0]){
return print("CORRECT ANSWER");
}
},
controller: _c,
textAlign: TextAlign.center,
enabled: true,
),
),),
Flexible(flex: 3,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data.length - 2,
itemBuilder: (context, index){
if(index>snapshot.data.length - 2){
return null;
}else{
return Padding(
padding: const EdgeInsets.all(4.0),
child: InputChip(
label: Text(snapshot.data[index + 2]),
onPressed: (){
_c.text += "${snapshot.data[index + 2]} ";
},
),
);
}
},
))
],
);
}
},
),)
],
)
);
}
}
Let's solve this in parts.
When I try to manually type it the FutureBuilder reloads when I enter and leave the TextField. What is the reason?
This is hapenning because when the keyboard is showing or hidding the flutter framework calls build method of your widget and this default behavior is the reason why your FutureBuilder is realoading. You should avoid call network methods inside build method and I advise you to use BLoC pattern to handle state of your widget.
My Future needs the String that is passed from another route, though. See the Arguments args = .... Any idea how I get it in the initState?
Well if you need context instance to get this String you can't access current context inside initState method because your widget isn't full initialized yet. A simple way to solve this in your case but not the best is verify if the data was already fetched from network or not.
Future _myNetworkFuture; // declare this as member of your stateWidgetClass
Widget build(BuildContext context){
final Arguments args = ModalRoute.of(context).settings.arguments;
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
// this line says if(_myNetworkFuture == null) do the thing.
_myNetworkFuture ??= getEverything(args.colName);
return ...
Flexible(flex: 5,
child: FutureBuilder(
future: _myNetworkFuture,
builder: (context, snapshot){
// ...
}
}
With this approach when flutter framework calls build method if you already fetched the data you don't download the data again. But I really advise you to use BLoC pattern in this kind of situation.