UI not properly updating after moving element from List<Widget> - flutter

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.

Related

flutter Call sequence of onTab function on ListView

I am trying with https://github.com/flutter/codelabs/blob/master/startup_namer/step6_add_interactivity/lib/main.dart everything works fine but
when i keep debugging point in the onTab function( At line number 61) and breakpoint in ListView.Builder( At line number 38 ).
OnTab method is getting called first after that only ListView is getting called but i'm not able to understand how the index are correctly calculated in onTap method because th actual logic for index is placed at ListView.
ListView
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return const Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
final alreadySaved = _saved.contains(_suggestions[index]);
OnTap
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(_suggestions[index]);
} else {
_saved.add(_suggestions[index]);
}
});
Please explain how the index is getting calculated onTap.
The favorite item is storing Set.
final _saved = <WordPair>{};
Once you click on favorite button, it checks whether it is already on _saved
final alreadySaved = _saved.contains(_suggestions[index]);
Now if alreadySaved is true, it remove from the current tap item from the set.
if (alreadySaved) {
_saved.remove(_suggestions[index]);
}
If it alreadySaved is false mean , _saved doesnt contain the item, it add it.
else {
_saved.add(_suggestions[index]);
}
It is not storing the index, it is storing items value
And _suggestions is holding all generated item.
variable "index" is under the scope of Item builder and ListView and its onTap function can access the variable from its stack memory.
https://www.geeksforgeeks.org/stack-vs-heap-memory-allocation/

Flutter : Update all items in a ListView

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(),

How to create buttons with for loop in a column in Flutter?

I need to implement lists of buttons in a column depending on the data entry. So, for that I have to use for loop. Each button requires two entires id, text. I can make it with List. But it accepts only string value not the integer.
This is the code I tried.
code
Widget getTextWidgets(List<String> strings)
{
List<Widget> list = new List<Widget>();
for(var i = 0; i < strings.length; i++){
list.add(new ButtonForHome(
lable: strings[i],
onPressed: (){},
));
}
return new Column(children: list);
}
I want to put id in onPressed event. How can I implement in the Flutter?
You should use Listview instead of Column
SAMPLE CODE
getTextWidgets() {
return ListView.builder(
itemCount: yourList.length,
itemBuilder: (context, itemIndex) {
return MaterialButton(
child: Text(yourList[itemIndex]),
onPressed: () {
debugPrint('Clicked index $itemIndex');
});
});
}
Now your question
I want to put id in onPressed event. How can I implement in the Flutter?
You can create a POJO class like this
class DataModel{
String name;
int id;
DataModel(this.name, this.id);
}
Now create list of your POJO class
List<DataModel> list= List<DataModel>();
Now add data in your list like this
list.add(DataModel("name", 1));
list.add(DataModel("name", 2));
list.add(DataModel("name", 3));
Now you can use it like this way
getTextWidgets() {
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, itemIndex) {
return MaterialButton(
child: Text(list[itemIndex].name),
onPressed: () {
debugPrint('Clicked item name '+list[itemIndex].name);
debugPrint('Clicked item ID '+list[itemIndex].id.toString());
});
});
}
Nilesh Rathod has indeed given the descriptive answer for the same. In flutter there is also, a way to achieve this, which is quite similar to POJO class, is
To create own widget and specify the fields needs to be passed when we are using the widget
Add the widget to the list, with the data specified for passing
You can track, the id, by pressing itself also
I can clearly see that, you have created your own widget named as ButtonForHome, which takes in label for now. What you can do is, to make your widget takes in two argument, and you can do it like this:
class ButtonForHome extends StatelessWidget {
final String label;
final int id; // this will save your day
// #required will not let user to skip the specified key to be left null
ButtonForHome({#required this.label, #required this.id});
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
color: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
child: Text(this.label),
onPressed: () => print(this.id) // Here you print your id using your button only
)
);
}
}
Now creating your button with a list, or adding via list
You can do it via ListView.builder()
You can do it via your way, i.e., List<Widget>.add()
I am gonna show the solution in your way only:
Widget getTextWidgets(List<String> strings){
List<Widget> list = new List<Widget>();
for(var i = 0; i < strings.length; i++){
list.add(ButtonForHome(
id: i, // passing the i value only, for clear int values
label: strings[i]
));
}
return Column(children: list);
}
With the new flutter in place, you don't need to do new every time while defining a widget. It understands now, so no need of const, new at all
So, wherever you populate your getTextWidget, it will show up the Widgte ButtonForHome, which has unique id, and label. Now the ButtonForHome, prints the id of that particular widget which was passed uniquely. So now, you can see your result happening.
I hope this is what you were looking for. Try it, and let me know.

setState does not seem to work inside a builder function

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((){});
},
)

Keep scroll position in ListView when adding new item on top

I am using a ListView.builder to render a chat.
The scenario is the following: A user types in a new chat message, when that happens i am storing the new message in state and therefore the itemCount is increased by one.
If i am at the end of the list, I see the new item directly at the top, everything is OK.
However, if I am in the middle of the chat and an item is added, the view scrolls a little bit. How can I disable this behaviour? I guess it kinda makes sense because the scroll offset is still the same double in the ScrollControllers state, but if there is one more item, it looks like it scrolls...
Is there a good way to do this? I guess the manual approach would be to measure the height of the new item and manually set the correct ScrollController offset again.. but meeeh
In fact, you should not add new chat items to the list. And you have to cache them and then inject the cache into the list after the list has reached the top or bottom. I hope this solution works for you.
This is a solution for items with static/constant height (height: 100) added on top of the list.
When I detect that list length has changed I change offset position by hardcoded item height by calling _scrollController.jumpTo(newOffect).
It works well. The list stays at the current position when a new item is added to the top, with no jumps visible.
class FeedWidget extends StatefulWidget {
FeedWidget({
Key key,
}) : super(key: key);
#override
State<StatefulWidget> createState() => _FeedWidgetState();
}
class _FeedWidgetState extends State<FeedWidget> {
var _scrollController = ScrollController();
var _itemsLength = 0;
#override
Widget build(BuildContext context) {
return StreamBuilder<List<FeedItem>>(
stream: Provider.of<FeedRepository>(context).getAll(),
builder: (BuildContext context, AsyncSnapshot<List<FeedItem>> items) {
print('Loaded feed data $items');
switch (items.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
final newItemsLength = items.data.length;
if (_itemsLength != 0 && _itemsLength != newItemsLength) {
final newItemsCount = newItemsLength - _itemsLength;
print('New items detected: $newItemsCount');
final newOffset = _scrollController.offset +
100 * newItemsCount; //item height is 100
print(
'Setting new offest ${_scrollController.offset} -> $newOffset');
_scrollController.jumpTo(newOffset);
}
print('Items length new = $newItemsLength old = $_itemsLength}');
_itemsLength = newItemsLength;
return Flexible(
child: ListView(
key: PageStorageKey('feed'), //keeps scroll position
controller: _scrollController,
padding: EdgeInsets.only(top: 8, bottom: 32),
children: items.data
.map((element) => FeedItemCard(item: element)) //item height is 100
.toList()));
}
},
);
}
}
You can use the flutter_scrollview_observer lib to implement your desired functionality without invasivity.
We only need three steps to implement the chat session page effect.
1、All chat data are displayed at the top of the listView when there is less than one screen of chat data.
2、When inserting a chat data
If the latest message is close to the bottom of the list, the listView will be pushed up.
Otherwise, the listview will be fixed to the current chat location.
Step 1: Initialize the necessary ListObserverController and ChatScrollObserver.
/// Initialize ListObserverController
observerController = ListObserverController(controller: scrollController)
..cacheJumpIndexOffset = false;
/// Initialize ChatScrollObserver
chatObserver = ChatScrollObserver(observerController)
..toRebuildScrollViewCallback = () {
// Here you can use other way to rebuild the specified listView instead of [setState]
setState(() {});
};
Step 2: Configure ListView as follows and wrap it with ListViewObserver.
Widget _buildListView() {
Widget resultWidget = ListView.builder(
physics: ChatObserverClampinScrollPhysics(observer: chatObserver),
shrinkWrap: chatObserver.isShrinkWrap,
reverse: true,
controller: scrollController,
...
);
resultWidget = ListViewObserver(
controller: observerController,
child: resultWidget,
);
return resultWidget;
}
Step 3: Call the [standby] method of ChatScrollObserver before inserting or removing chat data.
onPressed: () {
chatObserver.standby();
setState(() {
chatModels.insert(0, ChatDataHelper.createChatModel());
});
},
...
onRemove: () {
chatObserver.standby(isRemove: true);
setState(() {
chatModels.removeAt(index);
});
},