flutter - SliverList / SliverChildBuilderDelegate supply initial index or allow negative indices - flutter

I'm currently building a calendar view in Flutter using a SliverList and a SliverChildBuilderDelegate such that I don't have to render every single item in the calendar at once.
The first date is epoch time, Jan 1, 1970, and the last date is some odd amount of time computed after today's date.
My issue is is that when I first render the view, I want it to render the view starting today, not on Jan 1, 1970. However, if I have today as the 0 indexes, negative indices are not allowed (or supplied) to the builder delegate so you can't scroll up from that date. You also can't supply an initial index, as far as I can tell, to the builder or the list, so I can't make epoch time as the 0 indexes either since the list will just start there, making for quite the terrible experience! I'm not entirely sure how to proceed.
Does anyone have any suggestions?

I'm not aware of an easy way to do this, there's no initialPositition parameter in ListView nor in SliverList. The reason I can think of is that lists are a series of widgets embedded on a ScrollView, such that in order for you to set an initial item, you would need to know the exact scroll offset of that item.
By default the two list widgets make no assumption about the height of its items, so in general finding that offset would require you to compute the heights of all widgets before it one by one, which is inefficient.
However, you can make things easier if you know beforehand the height of all your list items, or if you can force them a fixed height through either the ListView.itemExtent field or the SliverFixedExtentList.
In case you do know (or forced) the height of your list items beforehand, you can set an initial item through an initialScrollOffset in your ScrollController. Here's an example with a ListView.
#override
Widget build(BuildContext context) {
final _itemExtent = 56.0; // I know item heights beforehand
final generatedList = List.generate(500, (index) => 'Item $index');
return ListView(
controller: ScrollController(initialScrollOffset: _itemExtent * 401),
children: generatedList
.map((index) =>
ListTile(title: Text(index, style: TextStyle(fontSize: 20.0))))
.toList(),
);
}
Or in a SliverList.
#override
Widget build(BuildContext context) {
final _itemExtent = 56.0;
final generatedList = List.generate(500, (index) => 'Item $index');
return CustomScrollView(
controller: ScrollController(initialScrollOffset: _itemExtent * 401),
slivers: [
SliverFixedExtentList(
itemExtent: _itemExtent, // I'm forcing item heights
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text(
generatedList[index],
style: TextStyle(fontSize: 20.0),
),
),
childCount: generatedList.length,
),
),
],
);
}
In both cases this is the result when you first open the app.

Related

Flutter: scrolling a list of DropdownButtons each containing 1k selections is too slow

I have a list of items (5-6 items) that are displayed using a ListView.builder. Each item contains a DropdownButton widget for the user to pick a number from 1 - 1000, thus containing 1000 DropdownMenuItems.
I implemented it as shown below, but the problem is that scrolling down the ListView is too slow and stutters. Even if the listView has 5 or 6 items, but note that each of them has an embedded DropdownButton containing 1000 DropdownMenuItems.
Is there a fix? Or another way to achieve my requirement?
N.B: Even if I reduce the number of DropdownMenuItems to 100, it still stutters when scrolling down the ListView.
class List extends StatelessWidget {
final List<Item> // Contains 5 items.
final List<int> quantityList = List<int>.generate(1000, (int i) => i);
//--
child: ListView.builder(
itemBuilder: (buildContext, i) {
return MyItem(
quantityList,
);
},
itemCount: items.length(),
)
class MyItem extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
child: DropdownButton<int>(
items: quantityList
.map(
(int e) =>
DropdownMenuItem<int>(
value: e,
child: Text(e.toString()),
),
)
.toList(),
),
),
);
}
Edit
I changed MyItem class to be as below, but still, the same problem exists.
Tried using ListView and ListView.custom instead of ListView.builder, to build the whole list in advance instead of lazily according to this, but still same issue.
I also tried running the app using --profile configuration to simulate a release version. The performance is better but still suffers from terrible stuttering and lag. Tested on emulator and physical device.
class MyItem extends StatelessWidget {
List<DropDownMenuItem> quantityList; // contains 1k
#override
Widget build(BuildContext context) {
return Container(
width:300,
height:300,
child: DropdownButton<int>(
items: quantityList,
),
),
);
}
ListView will create and destroy widgets as they come in and out of view. Your MyItem widget is a very heavy widget (lots of stuff to create and destroy).
You probably don't want each MyItem to be a Scaffold(). Normally you only have 1 Scaffold() visible as it's a fancy root view for an app. It has an app bar, snack bar, drawer, etc. Try having just your Container(...) that's currently under body: as the return from your MyItem.build().
In the items: of your DropdownButton, you build and destroy the list of items when the DropdownButton scrolls in and out of view. If this list is the same for every widget in your list like in your test code above, create one List<Widget>[] that contains your DropdownMenuItem widgets and pass it in to your MyItem() widgets. Something like:
//In your widget with the ListView
List<Widget> myItems;
//In the initState() of your widget with the ListView
...
myItems = quantitySelection.map(
(int e) => DropdownMenuItem<int>(
value: e,
child: Text(e.toString()),
),
).toList(),
...
//In your ListView.builder()
return MyItem(
...
items: myItems,
...
);
//In your MyItem.build() -> DropdownButton()
...
DropDownButton(
items: items
),
...
FWIW - we have a ListView with complex children that we test with 10,000 items. There's a significant difference in performance between the debug and release builds in Flutter. It stutters a little in the debug builds but is very smooth in the release builds.
I was able to solve the issue by only using the cacheExtent property of ListView.builder, setting it to list.length*200. This is kind of a workaround knowing that in my case the list length will always be small.
Pre-building the DropDownMenuItems had no sensed performance enhancement by a user, but it is a good recommended practice regardless, as instead of building the same DropDownMenuItems over and over again for every list item.
Although according to the docs: ListView and ListView.separated does not lazy load items rather build them all up at the beginning, I kept experiencing the same stuttering and lag during scrolling as with ListView.builder.

Dart/Flutter: horizontally scrollable week view - how to display particular items from the ListView?

I've been trying to make horizontal weekly calendar without using third party packages. I've created a horizontally scrollable Listview, containing three weeks : previous, current and next. Obviously, Listview displays from the first item in the list, which is the first day of the previuos week. Is there any way to control the items displayed and to display the current week with previous and next weeks being accessed through horizontal scrolling?
Here is the code for the ListView.builder:
Container(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: threeWeeks2.length,
itemBuilder: (BuildContext context, int index) {
var date = DateFormat('d').format(threeWeeks2[index]);
return DayCardBig(date);
}
),
),
Also, I can't keep but wondering whether the logic behind this implementation of week view is sound. I tried a more complex widget using carousel slider and pageview, but it just got messy. Is there any other widget that I'm not familiar with that I could use for a scrollable week view ?

How to add items to a Row object at runtime in Flutter?

The most amazing part of Flutter is that every widget is an object. But, when I try to make an object from a Row and add a List of widgets to its children, I get this error on runtime:
Cannot add to an unmodifiable list
I'm currently creating my Row like this:
Row rowStar = Row();
rowStar.children.addAll(rowChildren);
Is this wrong? If so, what's the correct way?
1. Why it won't work
If all you want to do is add a list to a Row, you should create the list and then initialize the Row with it. You cannot add the list afterwards because Flutter's widgets typically have final fields/properties, which are not modifiable at runtime.
2. The Flutter Way
However, what you could do to dynamically modify the list is pass a function that builds your list internally at runtime. And that's exactly what the ListView.builder() constructor of ListView does.
For example, this the docs example for dynamically creating a List of Containers based on the the Lists entries and colorCodes.
final List<String> entries = <String>['A', 'B', 'C'];
final List<int> colorCodes = <int>[600, 500, 100];
ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: entries.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
color: Colors.amber[colorCodes[index]],
child: Center(child: Text('Entry ${entries[index]}')),
);
}
);
You could add a button to .add, say, the letter D and the colorCode 400, to the respective Lists, and Flutter would render the respective Containers dynamically on the screen.
I'm not sure if I get it right, but what you're trying to do doesn't make sense. If you want to reuse a Row with widgets and information you can just build it once and save the whole thing as widget.
Try it like this:
Build a Row.
Create widgets for your children.
Display them as widgets of the row.
Row(
children: <Widget>[
customWidget1(),
customWidget2(),
customWidget3(),
],
)
Your custom widgets are the content of the Row then.
Hope it helps.
In my case, I can make a Row from a previously created List<Widget>:
Row calculateListOfStars(ranking) {
final List<Widget> rowList = [];
rowList.addAll([
// make my custom widget
]);
final Row rowStars = Row(
children: rowList,
);
return rowStars;
}
Use Listview:
ListView(
children: rowStar((data) {
return ListTile();
}).toList();

How to use detect changes in elements of an array using provider?

I started developing my first app with flutter and I have some questions.
I have a screen that contains a Horizontal ListView where each item represents a month and occupy the entire screen. Each item is another list of itens. It's basically a list showing expenses for every month. I'm controlling state using Provider, and I'm having trouble to detect changes in a month. For example, I'm in january, but I added an expense in february, so the february item must detect the change and rewrite. I haven't been able to do it so far. Here is the screenshots of the app:
I have a provider that has an array of "Periods". And a "Period" is a class that represents a month, and contains an array of expenses.
I guess I'm not organising my data the right away, I'd appreciate if someone could point me in the right direction.
EDIT:
So, I watched this video here: https://www.youtube.com/watch?v=d_m5csmrf7I, and that helped me a little at least on how to detect changes in nested providers. I basically did this:
Widget build(BuildContext context) {
...
final size = MediaQuery.of(context).size;
final financeiro = Provider.of<Financeiro>(context);
return ImprovedListView.builder(
controller: _listController,
scrollDirection: Axis.horizontal,
itemCount: financeiro.periodos.length,
itemSize: size.width,
physics: const NeverScrollableScrollPhysics(),
initialScrollOffset: financeiro.indexMesAtual * size.width,
itemBuilder: (ctx, index) {
final periodo = financeiro.periodos[index]; //get a piece of the list and pass to another change notifier provider
return ChangeNotifierProvider<Periodo>.value(
value: periodo,
child: PeriodoFinanceiro(
key: ValueKey('${periodo.ano}${periodo.mes}'),
index: index,
scrollTo: scrollTo,
carregarDadosIniciais: carregarDadosIniciais,
excluirLancamento: excluirLancamento,
),
);
};
}
Now I just have to figure out how to better control the scrolloffset of the list when its re-rendered.
Thanks

Jumping to a new page in PageView.builder after the additon of a new page doesn't seem to work as expected [Flutter]

I have a simple layout that consists of a PageView.builder widget that contains several pages. The contents of each page are simple, they contain a Container and a Text widget inside of it.
cards is a List of type String and the Pageview.builder widget has an itemCount that's based on the length of this List. The value of the Text in each page is assigned using this List.
List<String> cards = [];
Now, whenever I add a new value to cards List, a variable newPage is used to store the last index position in the List after that element has been added.
After doing this, setState(() {}); is called so that the UI along with the PageView update to reflect the changes made in the List.
The PageView widget does reflect the changes and a new page does get added to it.
However, the problem arises when I try to jump to newly added page right after calling setState.
The error indicates that the index value that jumpToPage is trying to use is out of range in the PageView
cards.add("New card");
newPage = cards.length - 1;
setState(() {});
card_PageController.jumpToPage(newPage);
So, after trying to figure something out, I added a Timer after the setState, so that the framework get's some time to properly update the UI elements.
I'm using a small value of 50 milliseconds in the Timer function and jumping to the new page after the timer gets over.
cards.add("New card");
newPage = cards.length - 1;
setState(() {});
Timer(Duration(milliseconds: 50), () {
card_PageController.jumpToPage(newPage);
})
The addition of a Timer seems to solve the problem and there were no errors after it's addition. However, I'm not sure if this is the right way of tackling this problem.
I'd like to know as to why is this happening, as shouldn't calling jumpToPage directly after setState work without the use of a Timer?
Also, does setState infact take some time to finish updating the UI, even though it isn't async, and that due to this reason the referenced index is invalid? And could this problem have been tackled in a better way?
From the code you shared I can't detect an issue. Especially because I've reproduced what you said you wanted to achieve and it works without issue. Take a look at the code below:
class PageCards extends StatefulWidget {
#override
_PageCardsState createState() => _PageCardsState();
}
class _PageCardsState extends State<PageCards> {
PageController _pageController = PageController();
List<String> cards = [
'page 0',
'page 1',
'page 2',
];
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: cards.length,
itemBuilder: (context, index){
return Center(
child: Text(cards[index]),
);
}
),
),
RaisedButton(
child: Text('Add page'),
onPressed: () => addCard(),
),
],
);
}
void addCard(){
setState(() {
cards.add('page ${cards.length}');
});
_pageController.jumpToPage(cards.length - 1);
}
}