I'm just starting to look at alternatives to setState() and therefore Provider. In the first program that I am looking at to implement Provider, the only setState() that I use is on the initial build(), and also when data changes in order to display a FAB when the FAB is not already displayed. There are six TextField widgets in this program, and they all have TextEditingControllers, so they all have their own state.
Is it feasible to use Provider in this situation when _tfDataHasChanged (bool) changes, and if so, how?
Code is below for the FAB creation:
Widget _createFab() {
if (_tfDisplayOnly || !_tfDataHasChanged) return null;
return FloatingActionButton(
onPressed: _btnSubmitPressed,
backgroundColor: _colorFab,
mini: false,
tooltip: _sBtnSubmitText /* Add or Create */,
child: _iconFab,
);
}
You can:
Widget _createFab( BuildContext context) {
boolean showFab = Provider.of<ShowFab>(context).shouldShow;
return showFab?FloatingActionButton(
onPressed: _btnSubmitPressed,
backgroundColor: _colorFab,
mini: false,
tooltip: _sBtnSubmitText /* Add or Create */,
child: _iconFab,
):null;
}
with the ShowFab class looking like
class ShowFab with ChangeNotifier {
boolean shouldShow;
void showFab( boolean show ){
shouldShow = show;
notifyListeners();
}
}
Obviously your specific logic will be different,
The ShowFab needs to be provided higher in the widget tree.
ChangeNotifierProvider<ShowFab>( builder: (context) => ShowFab(), child: .... )
Related
I work on flutter project . when i click to modify icon to edit name for example ==> the screen is roaleded automatically . How i can stop refresh screen after click on edit button ?
this piece of my Form code :
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Adresse email :',
style: TextStyle(
color: Color(0xFF4053FCF),
fontSize: 16,
fontWeight: FontWeight.w600
),
),
IconButton(
icon: Icon(CommunityMaterialIcons.pencil,
color: Colors.grey,
),
onPressed: () {
emailNode.requestFocus();
setState(() {
enableemail = true;
});
})
],
),
void editUserProfile() async {
setState(() {});
// if (_formKey.currentState.validate()) {
String name = _nameController.text;
String email = _emailController.text;
String adress = _adressController.text;
userApi.editUserProfile(name, email, adress).then((data) {
print(data);
if (data != null) {
// Navigator.pop(context);
/* Navigator.push(
context, MaterialPageRoute(builder: (context) => Profile()));*/
}
// setState(() {});
/* Navigator.push(
context, MaterialPageRoute(builder: (context) => BoxSettings()));*/
setState(() {
enableup = false;
enableadress = false;
enableemail = false;
});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(data)));
// ScaffoldMessenger.of(context).showSnackBar(snackBar3);
}).catchError((error) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.toString())));
});
setState(() {});
}
and this my screen for more information :
How i can press on edit button without reload screen ?
There some workarounds to achieve this (i.e. update the state of one widget after tapping a completely different widget) like passing the callback function as a parameter etc.
But The best and neat solution here which will solve the above problem and keep your code neat is using Provider pattern.
If you are not aware of how a Provider pattern works, you can easily google search for articles regarding it. Here is one of them :
https://www.raywenderlich.com/6373413-state-management-with-provider
Read the above article before moving below.
Basically what we do is :
Create a ChangeNotifier class.
Wrap the parent of both widgets by a ChangeNotifierProvider widget.
Wrap the widget you want to update with Consumer widget.
Then in your onTap/onPressed function of Edit button you can call a function which will call the notifyListener() function. What this will do is it will notify the above ChangeNotifierProvider widget that some change has neen occured in it's widget tree. Then it will traverse the child whole widget tree below and will update the widget wrapped with Consumer widget.
So this way, you wont need to refresh your whole screen and you can easily update one widget by doing some action on a competely different widget.
Wrap the widgets you want to refresh inside stateful builder and make the whole screen a stateless widget and then call stateful builder
I have the main/homepage widget of my app, let's call it home.dart.
Inside this widget, I have defined the drawer key in my Scaffold widget. The code for the Drawer object is in a separate file, navdrawer.dart.
home.dart
import 'navdrawer.dart';
. . .
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: NavDrawer(),
...
Now inside NavDrawer, I construct my Drawer widget which has a settings button, which links to the settings.dart screen.
Which I do like this:
navdrawer.dart
. . .
InkWell(
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
print(result);
},
child: ListTile(
leading: Icon(
Icons.settings,
color: AppTextColor,
),
title: Text('Settings'))),
So now, when the user presses the back button on the settings page, the Navigator.pop() call will return the data I need to the result variable in navdrawer.dart.
But my problem is ... how do I get this data to my home.dart screen/state?
I'll suggest you to use provider, scoped_model or other state management techniques for this. The easiest (but also the worthless) solution would be to use a global variable.
However, there's a middle ground. For simplicity I'm using dynamic for the type of result, you'd better know what Settings return, so use that type instead.
Your home.dart file
class _HomePageState extends State<HomePage> {
dynamic _result; // Create a variable.
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: NavDrawer(
onResult: (result) {
_result = result; // <-- This is your result.
}
),
);
}
}
Add following in your navdrawer.dart:
class NavDrawer extends StatelessWidget {
// Add these two lines.
final ValueChanged onResult;
NavDrawer({this.onResult});
// Other code ...
}
Modify your onTap method inside navdrawer.dart file:
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
onResult(result); // <-- Add this line.
}
Please set parameters into the pop method.
Like
Navigator.pop(context,true)
Define 'Static' global variable in homepage screen/widget
Then call the variable from anywhere :
1- homepage :
Static String getData;
2- when returned to Drawer :
homepage.getData=value;
Navigator.pop();
I am trying to run a function(with arguments) inside two-levels down StateFul widget, by clicking a button in the parent of the parent of that child(after having all widgets built, so not inside the constructor). just like in the image below:
More details is that I created a Carousal which has Cards inside, published here.
I created it with StreamBuilder in mind(this was the only use case scenario that I used it for so far), so once the stream send an update, the builder re-create the whole Carousal, so I can pass the SELECTED_CARD_ID to it.
But now I need to trigger the selection of the carousal's Cards programmatically, or in another word no need for two construction based on the snapshot's data like this:
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableCarousal(
selectedId: snapshot.data.toInt(),
onTap: //Update the stream
//some props...,
);
} else {
return SelectableCarousalLoading(
selectedId: null,
onTap: //Update the stream
//some props...,
);
}
},
);
But instead, I'm trying to have something like this so I can use it for others use cases:
Widget myCarousal = SelectableCarousal(
selectedId: null,
onTap: //Update the stream
//some props...,
);
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
// Then when data ready I can update
// the selection by calling two-level down function
if (snapshot.hasData) {
myCarousal.selectById(3);
}
// Build same carousal in all cases.
return myCarousal;
},
);
so this led me to my original question "How to run a function(with arguments) inside two-levels down StateFul widget?".
I appreciate any help. thanks a lot.
I was able to solve that challenge using the BLoC & Stream & StreamSubscription, see the image below:
Inside the Homepage screen:
///...Inside Homepage screen level-0
RaisedButton(
child: Text('Update value in the BLoC'),
onPressed: () {
bloc.changeSelectedState(isSel);
},
),
//...
inside the BLoC:
class Bloc {
final BehaviorSubject<bool> _isSelectedStreamController = BehaviorSubject<bool>();
// Retrieve data from stream
Stream<bool> get isSelectedStream => _isSelectedStreamController.stream;
// Add data to stream
Function(bool) get changeSelectedState => _isSelectedStreamController.sink.add;
void dispose() {
_isSelectedStreamController.close();
}
}
final bloc = Bloc();
Inside any widget in any level as long as it can reach the bloc:
// This inside the two-levels down stateful widget..
StreamSubscription isSelectedSubscription;
Stream isSelectedStream = bloc.isSelectedStream;
isSelectedSubscription = isSelectedStream.listen((value) {
// Set flag then setState so can show the border.
setState(() {
isSelected = value;
});
});
//...other code
#override
Widget build(BuildContext context) {
return Container(
decoration: isSelected
? BoxDecoration(
color: Colors.deepOrangeAccent,
border: Border.all(
width: 2,
color: Colors.amber,
),
)
: null,
//...other code
);
}
so the new design of my widget includes the BLoC as a main part of it, see the image:
and...works like a charm with flexible and clean code and architecture ^^
On app homepage I set up Model2 which make API call for data. User can then navigate to other page (Navigator.push). But I want make API call from Model2 when user press back (_onBackPress()) so can refresh data on homepage.
Issue is Model2 is not initialise for all user. But if I call final model2 = Provider.of<Model2>(context, listen: false); for user where Model2 is not initialise, this will give error.
How I can call Provider only on condition? For example: if(user == paid)
StatefulWidget in homepage:
#override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<Model1, Model2>(
initialBuilder: (_) => Model2(),
builder: (_, model1, model2) => model2
..string = model1.string,
),
child: Consumer<Model2>(
builder: (context, model2, _) =>
...
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(context: context)),
In Page2:
Future<void> _onBackPress(context) async {
// if(user == paid)
final model2 = Provider.of<Model2>(context, listen: false);
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return
// if(user == paid)
Provider.value(value: model2, child:
AlertDialog(
title: Text('Back'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('Go back'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('OK'),
onPressed: () async {
// if(user == paid)
await model2.getData();
Navigator.of(context).pop();
},
),
],
),
);
},
);
}
Alternative method (maybe more easy): How to call provider on previous page (homepage) on Navigator.of(context).pop();?
TLDR: What is best solution for call API so can refresh data when user go back to previous page (but only for some user)?
You can wrap your second page interface builder in a WillPopScope widget, and then, pass whatever method you want to call to the onWillPop callback of the WillPopScope widget. This way, you can make your API call when user presses the back button. Find more about the WillPopScope widget on this WillPopScope Flutter dev documentation article.
tldr; Establish and check your single point of truth before the call to the Provider
that may result in a null value or evaluate as a nullable reference.
Perhaps you can change the architecture a bit to establish a single (nullable or bool) reference indicating whether the user has paid. Then use Darts nullability checks (or just a bool) to implement the behavior you want. This differs from your current proposal in that there would be no need to call on the Provider to instantiate the model. Just add a single point of truth to your User object that is initialized to null or false, and then change that logic only when the User has actually paid.
Toggling widgets/behavior in this way could be a solution.
Alternatives considered:
Packaging critical data points into a separate library so that the values can be imported where needed.
Other state management methods for key/value use.
If you want to simply hide/show parts of a page consider using the OffStage class or the Visibility class
Ref
Dart null-checking samples
How does setState actually work?
It seems to not do what I expect it to do when the Widget which should have been rebuilt is built in a builder function. The current issue I have is with a ListView.builder and buttons inside an AlertDialog.
One of the buttons here is an "AutoClean" which will automatically remove certain items from the list show in the dialog.
Note: The objective here is to show a confirmation with a list of "Jobs" which will be submitted. The jobs are marked to show which ones appear to be invalid. The user can go Back to update the parameters, or press "Auto Clean" to remove the ones that are invalid.
The button onTap looks like this:
GeneralButton(
color: Colors.yellow,
label: 'Clear Overdue',
onTap: () {
print('Nr of jobs BEFORE: ${jobQueue.length}');
for (int i = jobQueue.length - 1; i >= 0; i--) {
print('Checking item at $i');
Map task = jobQueue[i];
if (cuttoffTime.isAfter(task['dt'])) {
print('Removing item $i');
setState(() { // NOT WORKING
jobQueue = List<Map<String, dynamic>>.from(jobQueue)
..removeAt(i); // THIS WORKS
});
}
}
print('Nr of jobs AFTER: ${jobQueue.length}');
updateTaskListState(); // NOT WORKING
print('New Task-list state: $taskListState');
},
),
Where jobQueue is used as the source for building the ListView.
updateTaskListState looks like this:
void updateTaskListState() {
DateTime cuttoffTime = DateTime.now().add(Duration(minutes: 10));
if (jobQueue.length == 0) {
setState(() {
taskListState = TaskListState.empty;
});
return;
}
bool allDone = true;
bool foundOverdue = false;
for (Map task in jobQueue) {
if (task['result'] == null) allDone = false;
if (cuttoffTime.isAfter(task['dt'])) foundOverdue = true;
}
if (allDone) {
setState(() {
taskListState = TaskListState.done;
});
return;
}
if (foundOverdue) {
setState(() {
taskListState = TaskListState.needsCleaning;
});
return;
}
setState(() {
taskListState = TaskListState.ready;
});
}
TaskListState is simply an enum used to decide whether the job queue is ready to be submitted.
The "Submit" button should become active once the taskListState is set to TaskListState.ready. The AlertDialog button row uses the taskListState for that like this:
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
if (taskListState == TaskListState.ready)
ConfirmButton(
onTap: (isValid && isOnlineNow)
? () {
postAllInstructions().then((_) {
updateTaskListState();
// navigateBack();
});
: null),
From the console output I can see that that is happening but it isn't working. It would appear to be related to the same issue.
I don't seem to have this kind of problem when I have all the widgets built using a simple widget tree inside of build. But in this case I'm not able to update the display of the dialog to show the new list without the removed items.
This post is getting long but the ListView builder, inside the AleryDialog, looks like this:
Flexible(
child: ListView.builder(
itemBuilder: (BuildContext context, int itemIndex) {
DateTime itemTime = jobQueue[itemIndex]['dt'];
bool isPastCutoff = itemTime.isBefore(cuttoffTime);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text(
userDateFormat.format(itemTime),
style: TextStyle(
color:
isPastCutoff ? Colors.deepOrangeAccent : Colors.blue,
),
),
Icon(
isPastCutoff ? Icons.warning : Icons.cached,
color: isPastCutoff ? Colors.red : Colors.green,
)
],
);
},
itemCount: jobQueue.length,
),
),
But since the Row() with buttons also doesn't react to setState I doubt that the problem lies within the builder function itself.
FWIW all the code, except for a few items like "GeneralButton" which is just a boilerplate widget, resides in the State class for the Screen.
My gut-feeling is that this is related to the fact that jobQueue is not passed to any of the widgets. The builder function refers to jobQueue[itemIndex], where it accesses the jobQueue attribute directly.
I might try to extract the AlertDialog into an external Widget. Doing so will mean that it can only access jobQueue if it is passed to the Widget's constructor....
Since you are writing that this is happening while using a dialog, this might be the cause of your problem:
https://api.flutter.dev/flutter/material/showDialog.html
The setState call inside your dialog therefore won't trigger the desired UI rebuild of the dialog content. As stated in the API a short and easy way to achieve a rebuild in another context would be to use the StatefulBuilder widget:
showDialog(
context: context,
builder: (dialogContext) {
return StatefulBuilder(
builder: (stateContext, setInnerState) {
// return your dialog widget - Rows in ListView in Container
...
// call it directly as part of onTap of a widget of yours or
// pass the setInnerState down to another widgets
setInnerState((){
...
})
}
);
EDIT
There are, as in almost every case in the programming world, various approaches to handle the setInnerState call to update the dialog UI. It highly depends on the general way of how you decided to manage data flow / management and logic separation. As an example I use your GeneralButton widget (assuming it is a StatefulWidget):
class GeneralButton extends StatefulWidget {
// all your parameters
...
// your custom onTap you provide as instantiated
final VoidCallback onTap;
GeneralButton({..., this.onTap});
#override
State<StatefulWidget> createState() => _GeneralButtonState();
}
class _GeneralButtonState extends State<GeneralButton> {
...
#override
Widget build(BuildContext context) {
// can be any widget acting as a button - Container, GestureRecognizer...
return MaterialButton(
...
onTap: {
// your button logic which has either been provided fully
// by the onTap parameter or has some fixed code which is
// being called every time
...
// finally calling the provided onTap function which has the
// setInnerState call!
widget.onTap();
},
);
}
If you have no fixed logic in your GeneralButton widget, you can write: onTap: widget.onTap
This would result in using your GeneralButton as follows:
...
GeneralButton(
...
onTap: {
// the desired actions like provided in your first post
...
// calling setInnerState to trigger the dialog UI rebuild
setInnerState((){});
},
)