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/
Related
I am trying the widgets.AnimatedGrid.1 mysample of AnimatedGrid class documentation and I am always getting a RangeError (index): Invalid value: Not in inclusive range whenever I replace at runtime the late ListModel<int> _list in _AnimatedGridSampleState with a new shorter list.
Simply replacing the code of _insert handler with:
void _insert() {
setState(() {
_list = ListModel<int>(
listKey: _gridKey,
initialItems: <int>[7, 6, 5],
removedItemBuilder: _buildRemovedItem,
);
});
}
then clicking on + button will throw a RangeError.
Since build() in AnimatedGridSampleState depends of _list I was expecting that it will build a new AnimatedGrid with the correct initialItemCount and avoiding RangeError:
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: ...,
body: Padding(
padding: const EdgeInsets.all(16.0),
child: AnimatedGrid(
key: _gridKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
),
),
);
}
Yet, the _buildItem(...) it is still being called with the same indexes of the former longer _list. Why?
You can try it by yourself running on the browser in the snippet container of AnimatedGrid page, replacing _insert() code just like shown in the following print screens. You will not see the RangeError but you will see that former items 4, 5, 6 remain on the AnimatedGrid.
To remove items takes Duration(milliseconds: 300). So setState try to rebuild the items meanwhile and cause the issue. In order to overcome this issue, I came up with removing one by one and then inserting item, created another two method on the ListModel.
class ListModel<E> {
.....
void clear() {
for (int i = _items.length - 1; i >= 0; i--) {
removeAt(i);
}
}
void addAll(List<E> item) {
for (int i = 0; i < item.length; i++) {
insert(i, item[i]);
}
}
Now while you like to reset the item.
void _insert() async {
_list.clear();
/// delay to looks good; kDuration takes to remove item, therefore I am using Future method.
await Future.delayed(const Duration(milliseconds: 300));
setState(() {
_list.addAll(<int>[7, 6, 5]);
});
}
it would be better if you could share the function responsible for removing the item but i can guess what might be the problem if you are getting this error when removing the last item in the list
if you are removing an item then you need to be careful about something
first let's take a look at the removing function
E removeAt(int index) {
final E removedItem = _items.removeAt(index);
if (removedItem != null) {
_animatedGrid!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
},
);
}
return removedItem;
as you can see at the start of the function we are using the index to remove the item we want to remove and storing it in a new variable
then we are using it here
_animatedGrid!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
},);
as you can see we are using the item that we removed from the list because it will be displayed during the animation that's why we need the item but not the index
and we can't use the index directly in this part cause we already removed the item from the list so if we used it like that
_animatedGrid!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return removedItemBuilder(_items[index], context, animation);
},
);
you will be getting a RangeError (index): Invalid value: Not in inclusive range
because this item is already removed and so it's index is out of range
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 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.
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((){});
},
)
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);
});
},