I'm trying to create a simpler contextual action bar(CAB) https://pub.dev/packages/contextualactionbar
What I want to do is to update all the items in a ListView in Flutter.
For example, I want to display a trailing checkbox for each ListTile when I long press on an item. The item can be a stateful widget.
Here is a gif of what I want to do:
I tried to use a GlobalKey example from https://stackoverflow.com/a/57310380/5712419
Using a GlobalKey, only the visible items would update.
Update 28/04/2021: I answered this question with something that worked for me: the Provider package. I think it is cleaner than using a GlobalKey.
It's possible to select all items in a ListView by using Provider. https://pub.dev/packages/provider
These are the steps I took.
Step 1: Create a class that extends ChangeNotifier for a single item
class SelectableItemState extends ChangeNotifier {
bool _selectable = false;
bool get selectable => _selectable;
set selectable(bool value) {
_selectable = value;
notifyListeners();
}
}
Step 2: Register the Change Notifier
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => SelectableItemState()),
], //...
Step 3: Create the Consumer, and update the state from the gesture detector
return Consumer<SelectableItemState>(builder: (context, state, child) {
return GestureDetector(){
onLongPress: () {
state.selectable = true;
},
child: Material( // use state.selectable in the item widget...
}
}
Step 4: Update the trailing of your item (if selectable, show checkbox, else, show an empty container)
trailing: (state.selectable)
? Checkbox(
value: // state.selectedItem,
onChanged: (value) {
setState(() {
// Here we can use something like
// state.setSelectedItem(item, value);
});
},
)
: Container(),
Related
I'm using Provider package, and I want to dynamically add widgets to display.
I wrote the code like below, but the widgets doesn't show anything.
No errors have occurred.
// Contains widget and related data
class WidgetData {
Widget? child; // want to show this
String data1;
int data2;
}
class Model exteds ChangeNotifier {
List<WidgetData> widget; // I want to show all of this widget.child
void addWidget(Widget child) {
print("Called1") // "Called1"
var w = widgets.toList();
w.add(child);
widgetData = w;
notifyListeners();
}
}
class Example extends StatelessWidget {
#override
build (BuildContext context) {
return Scaffold(
body: Column(children: [
for(var i in context.watch<Model>().widget) i.child!;
]);
}
}
When the button pushed, context.read<Model>().addWidget(Text("test")) will be called.
But still doesn't show widgets.
// inside of build(BuildContext context)
FloatingActionButton(
onPressed: () => context.read<Model>().addWidget(Text("test")),
child: Icon(Icons.abc)
);
Of course I built the tree of provider in main.
void main() {
runApp(MultiProvider(
ChangeNotifierProvider<Model>(
create: (_) => Model()),
));
child:
....
}
It is discouraged to create and store Widgets outside of the build function in Flutter. Your provider should not provide Widgets, but the data needed to construct widgets, and you then construct the widgets inside the build method.
For example, if instead of a list of Widgets you had a list of Strings, then in the build method you convert that list of Strings easily into Text widgets like this: Column(children: stringlist.map((e) => Text(e)).toList())
I have a List<Entry> mySet, which is a List of widget Entry.
In a parent widget, I use a ListView.builder to generate these widgets.
This parent widget has two additional methods
addEntry() {
setState(() {
mySet.add(Entry(index: mySet.length));
});
}
removeEntry(_EntryState entry) {
int index = entry.widget.index;
setState(() {
mySet.removeWhere((item) => item.index == index);
});
}
The buttons that call these in each entry look like this
IconButton(
iconSize: 25,
icon: Icon(Icons.add),
onPressed: index == mySet.last.index
? () {
setState(() {
pKey.currentState!.addEntry();
});
}
: null),
IconButton(
iconSize: 25,
icon: Icon(Icons.remove),
onPressed: isLast(() {
setState(() {
pKey.currentState!.removeEntry(this);
});
})),
And this function
isLast(Function fn) => index == mySet.last.index ? fn : null;
The condition on the add and subtract functions are identical, this was just me testing and trying to debug the problem.
The problem:
When I click to add a row widget, the prior rows' add/remove buttons are disabled, as they should be.
But when I click remove on the last row, the prior row's buttons are not re-enabled.
Yet if I make a small change to the code (anything), to trigger a hot-reload, the last row's buttons enable.
Here's my Listview.builder code, in the same parent widget.
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return mySet[index];
},
itemCount: mySet.length);
}
The user in question Flutter: The widget values inside the ListView are not updated when an item is removed has a similar problem.
In your case you can solve this issue by using a ValueKey(property) where property is a a ofttribute your Entry object which you are sure is different for every Entry.
Make sure no two Entries can have the same value for property at the same time as otherwise flutter will throw an Error as you will have two identical ValueKeys.
I solved part of the problem by having one array that tracked instances of Entry, and one array to track corresponding states.
I could then call setState() for the last item which was the source of my trouble in my post.
removeEntry(_Entry entry) {
theSetStates.removeWhere((v) => v == entry);
theSet.removeWhere((v) => v == entry.widget);
setState(() {});
theSetStates.last.setState(() {});
}
If there's an easier way to get a list of instances of a Widget. Please let me know. Google didn't turn up anything.
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
So I am implementing something like below:
class TempProvider extends ChangeNotifier(){
List<Widget> _list = <Widget>[];
List<Widget get list => _list;
int _count = 0;
int get count => _count;
Future<List<Widget>> getList() async{
addToList(Text('$count'));
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
addToList(Widget widget){
_list.add(widget);
notifyListeners();
}
increment(){
_count += 1;
notifyListeners();
}
}
class Parent extends StatelessWidget{
#override
Widget build(BuildContext context) {
return FutureProvider(
create: (context) => TempProvider().getList(),
child: Child(),
);
}
}
class Child extends StatelessWidget{
#override
Widget build(BuildContext context) {
var futureProvider = Provider.of<List<Widget>>(context);
return Container(
child: futureProvider == null
? Text('Loading...'
: ListView.builder(
itemCount: futureProvider.length,
itemBuilder: (BuildContext context, int index){
return futureProvider[index];
}
),
));
}
}
Basically, what this does is that a List of Widgets from a Future is the content of ListView Builder that I have as its objects are generated from a database query. Those widgets are buttons that when pressed should update the "Count" value and should update the Text Widget displaying the latest "Count" value.
I was able to test the buttons and they seem to work and are incrementing the _count value via backend, however, the displayed "Count" on the Text Widget is not updating even if the Provider values are updated.
I'd like to ask for your help for what's wrong here, with my understanding, things should just update whenever the value changes, is this a Provider anti-pattern, do I have to rebuild the entire ListView, or I missed something else?
I'm still getting myself acquainted with this package and dart/flutter in general, hoping you can share me your expertise on this. Thank you very much in advance.
so I have been on a lot of research and a lot of trial and errors last night and this morning, and I just accidentally bumped into an idea that worked!
You just have to have put the listening value on a consumer widget making sure it listens to the nearest Provider that we have already implemented higher in the widget tree. (Considering that I have already finished drawing my ListView builder below the FutureProvider Widget)
..getList() async{
Consumer<ChallengeViewProvider>(
builder: (_, foo, __) => Text(
'${foo.count}',
),
);
List _result = await db....
_result.forEach((_item){
addToList(Button(
onTap: () => increment();
child: Text('Press'),
));
});
}
I have also refactored my widgets and pulled out the Button as a stateless widget for reuse. Though make sure that referenced Buttons are subscribed to the same parent provider having the Counter value and have the onTap property call out the increment() function through Provider<>.of
Hoping this will help anyone in the future!
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((){});
},
)