Flutter ListView.separated frame drop when loading huge list - smooth-scrolling

I'm trying to display a lazy list in flutter for windows. The list has approximately 2300 elements in it. The list is within a FutureBuilder, whose future is to fetch the 2300 elements from Hive database. Each element in the list is a MaterialButton with some properties. Im not getting smooth scrolling when scrolled fast. Some frames are being dropped. I have tried cacheextend and setting automatickeepalives to true. Still having the same problem. When ItemExtend is set to a large number(say 40), the scrollView works fine without frame drop. In release mode, it has better performance, but still some frames are being dropped. What is the best solution available to this problem?
//this Rawscrollbar is returned if the future is have some data
RawScrollbar(
isAlwaysShown: true,
controller: scrollControllerMLC,
thickness: context.percentWidth * .8,
radius: Radius.zero,
thumbColor:
SearchLeftContainerColors.headPoolListThumbColor,
child: ListView.separated(
padding: EdgeInsets.fromLTRB(
context.percentWidth * .5,
context.percentHeight * 0,
context.percentWidth * 1,
context.percentHeight * 1),
itemCount: lengthOfBoxes,
controller: scrollControllerMLC,
// addAutomaticKeepAlives: true,
// physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, int index) {
return ListButtonMEDLC(data[index], index);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(
color: SearchLeftContainerColors
.headsPoolSeparatorColour,
height: 1,
);
},
));
class ListButtonMEDLC extends StatelessWidget {
final String text;
final int index;
ListButtonMEDLC(this.text, this.index);
#override
Widget build(BuildContext context) {
return MaterialButton(
color:
(context.watch<ListButtonColorChangerMEDLC>().isSelectedList[index])
? SearchLeftContainerColors.headPoolListSelectedColor
: SearchLeftContainerColors.headPoolListColor,
hoverColor:
(context.watch<ListButtonColorChangerMEDLC>().isSelectedList[index])
? SearchLeftContainerColors.headPoolListSelectedColor
: SearchLeftContainerColors.headPoolListHoverColor,
highlightColor: SearchLeftContainerColors.headPoolListHighLightedColor,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
text,
style: TextStyle(
fontSize: context.percentWidth * 1.1,
color: (context
.watch<ListButtonColorChangerMEDLC>()
.isSelectedList[index])
? Colors.white
: Colors.black),
),
),
onPressed: () {
context.read<ListButtonColorChangerMEDLC>().changeIsSelectedList(index);
},
);
}
}
//this it the future of the future builder;
loadDrugBox() async {
Map boxes = await DB.boxes.getBoxAsMap("drugs");
lengthOfBoxes = boxes.length;
return boxes;
}
//Provider
class ListButtonColorChangerMEDLC extends ChangeNotifier {
List<bool> isSelectedList = List.generate(lengthOfBoxes, (index) => false);
changeIsSelectedList(int indexOfSelected) {
for (int i = 0; i < lengthOfBoxes; i++) {
if (i == indexOfSelected) {
isSelectedList[i] = true;
} else
isSelectedList[i] = false;
}
notifyListeners();
}
}

Yes. I solved this issue by replacing material button which was an expensive widget with gestureDetector.

Related

How to get original image size in Dart Image.network?

The following code gives Division by null error,
#override
Widget build(BuildContext context) {
Image image = Image.network(data['image-url']);
double widthToHeight = image.width / image.height;
if(widthToHeight <= 0.2) {
return NewsTileSmall(data: data);
} else {
return NewsTileLarge(data: data);
}
}
Best if, I could use a function to set the value of double widthToHeight.
In your build method you need to preload image.
Image image = new Image.network('image-url');
Completer<ui.Image> completer = new Completer<ui.Image>();
image.image
.resolve(new ImageConfiguration())
.addListener(ImageStreamListener(ImageInfo info, bool _) {
completer.complete(info.image));
})
Then you need to use FutureBuilder
FutureBuilder<ui.Image>(
future: completer.future,
builder: (BuildContext context, AsyncSnapshot<ui.Image> snapshot) {
if (snapshot.hasData){
return your image Widget}
else{
return placeholder widget
}
completed build method
import 'dart:ui' as ui;
Image image = Image.network('image url');
final completer = Completer<ui.Image>();
image.image
.resolve(const ImageConfiguration()).
addListener(ImageStreamListener((ImageInfo info, bool syncCall) => completer.complete(info.image)));
return Scaffold(
appBar: AppBar(
title: const Text("Image Dimensions Example"),
),
body: FutureBuilder<ui.Image>(
future: completer.future,
builder: (BuildContext context, AsyncSnapshot<ui.Image> snapshot) {
if (snapshot.hasData) {
return Text(
'${snapshot.data.width}x${snapshot.data.height}',
);
} else {
return const Text('Loading...');
}
},
)
);
});
To get the original size of a network image, you could use the image_pixels library.
Here is how to use it:
Widget buildContent(BuildContext context, String url) {
final width = MediaQuery.of(context).size.width; // This is the screen size width
final height = MediaQuery.of(context).size.height; // This is the screen size height
Image nimg = Image.network(url); // Preload the network image
return Stack(children: [
Center(
child: Image(
image: nimg.image,
fit: BoxFit.fill,
width: width,
height: height,
)
),
// Wrap the widget that need to access the original image size in ImagePixels
ImagePixels(
imageProvider: nimg.image,
builder: (context, img) {
return Positioned(
// Place a widget on top of the image, it's position related to the original image size
top: 92.0 * (height/img.height),
left: 137.0 * (width/img.width),
child: InkWell(
onTap: () {
print("On tap");
},
child:
Container(
width: 50,
height: 50,
color: Colors.red,
),
)
);
},
)
]);
}

Flutter AnimatedList still janky?

I'm currently using an AnimatedList in my Flutter app and having problems with the way removed list items are animated out. The animation itself works as expected but once the removed item finishes animating, it just disappears causing the other widgets to jump into its place. I had expected the other items to transition into the place of the removed item ...
I tried wrapping my list items with a ScaleTransition but that didn't help - the other list items still do not react to the removed item until it has finished the animation.
This kind of defies the purpose of AnimatedList, right? Or did I do something wrong? The "Widget of the week" video about AnimatedList clearly shows that list items react to newly inserted items by changing their position ...
Here is my code:
#override
Widget build(BuildContext context) {
return AnimatedList(
padding: EdgeInsets.only(top: REGULAR_DIM,
bottom: REGULAR_DIM + kBottomNavigationBarHeight),
initialItemCount: data.length,
itemBuilder: (context, index, animation) {
return MyCustomWidget(
data: data[index],
animation: animation,
disabled: false
);
},
);
}
class MyCustomWidget extends AnimatedWidget {
final MyModel data;
final bool disabled;
MyCustomWidget({
#required this.data,
#required Animation<double> animation,
this.disabled = false
}) : super(listenable: animation);
Animation<double> get animation => listenable;
#override
Widget build(BuildContext context) {
final content = ... ;
return ScaleTransition(
scale: CurvedAnimation(
parent: animation,
curve: Interval(0, 0.25)
).drive(Tween(begin: 0, end: 1)),
child: FadeTransition(
opacity: animation,
child: SlideTransition(
position: animation.drive(
Tween(begin: Offset(-1, 0), end: Offset(0, 0))
.chain(CurveTween(curve: Curves.easeOutCubic))),
child: content,
),
),
);
}
}
And then somewhere in the MyCustomWidget I invoke this function:
void _remove(BuildContext context) async {
final animatedList = AnimatedList.of(context);
// obtain myModel asynchronously
myModel.removeData(data);
animatedList.removeItem(index, (context, animation) => MyCustomWidget(
data: data,
animation: animation,
disabled: true,
), duration: Duration(milliseconds: 350));
}
The key is to trigger two Transitions one SlideTranstion() and another SizeTransition to eliminate to jump when the item is removed
here is some sample code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text('Update AnimatedList data')),
body: BodyWidget(),
),
);
}
}
class BodyWidget extends StatefulWidget {
#override
BodyWidgetState createState() {
return new BodyWidgetState();
}
}
class BodyWidgetState extends State<BodyWidget>
with SingleTickerProviderStateMixin {
// the GlobalKey is needed to animate the list
final GlobalKey<AnimatedListState> _listKey = GlobalKey(); // backing data
List<String> _data = ['Horse', 'Cow', 'Camel', 'Sheep', 'Goat'];
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(
height: 400,
child: AnimatedList(
key: _listKey,
initialItemCount: _data.length,
itemBuilder: (context, index, animation) {
return _buildItem(
_data[index],
animation,
);
},
),
),
RaisedButton(
child: Text(
'Insert single item',
style: TextStyle(fontSize: 20),
),
onPressed: () {
_onButtonPress();
},
),
RaisedButton(
child: Text(
'Remove single item',
style: TextStyle(fontSize: 20),
),
onPressed: () {
_removeSingleItems();
},
),
],
);
}
Widget _buildItem(String item, Animation<double> animation, {direction: 0}) {
return (direction == 0)
? SizeTransition(
sizeFactor: animation,
child: Card(
color: Colors.amber,
child: ListTile(
title: Text(
item,
style: TextStyle(fontSize: 20),
),
),
),
)
: Stack(
children: [
SizeTransition(
sizeFactor: animation,
child: Card(
color: Colors.transparent,
child: ListTile(
title: Text(
item,
style: TextStyle(fontSize: 20),
),
),
),
),
Align(
alignment: Alignment.topCenter,
heightFactor: 0,
child: SlideTransition(
position: animation
.drive(Tween(begin: Offset(-1, 0), end: Offset(0, 0))),
child: Card(
color: Colors.red,
child: ListTile(
title: Text(
item,
style: TextStyle(fontSize: 20),
),
),
),
),
),
],
);
}
void _onButtonPress() {
_insertSingleItem();
}
void _insertSingleItem() {
String item = "Pig";
int insertIndex = 2;
_data.insert(insertIndex, item);
_listKey.currentState.insertItem(insertIndex);
}
void _removeSingleItems() {
int removeIndex = 2;
String removedItem = _data.removeAt(removeIndex);
// This builder is just so that the animation has something
// to work with before it disappears from view since the
// original has already been deleted.
AnimatedListRemovedItemBuilder builder = (context, animation) {
// A method to build the Card widget.
return _buildItem(removedItem, animation, direction: 1);
};
_listKey.currentState.removeItem(removeIndex, builder);
}
void _updateSingleItem() {
final newValue = 'I like sheep';
final index = 3;
setState(() {
_data[index] = newValue;
});
}
}
enter code here
You need to test the performance with the release version of your app.

How to scroll or jump to position of PageView.builder or PageController in Flutter?

Issue: Unable to scroll to POSITION after loading the Pageviews using PageController *
like ViewPager scroll to specific page in Android
Widget _buildCarousel(BuildContext context, int selectedIndex) {
PageController controller = PageController(viewportFraction: 1, keepPage: true);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
// you may want to use an aspect ratio here for tablet support
height: 400.0,
width: 240,
child: PageView.builder(
itemCount: assetImageList.length,
controller: controller,
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, selectedIndex, itemIndex);
},
),
)
],
);
}
Finally found the answer. Just set the initialPage: mSelectedPosition attribute like:
child: PageView.builder(
itemCount: mTemplateModelList.length,
controller: PageController(initialPage: mSelectedPosition, keepPage: true, viewportFraction: 1),
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, selectedIndex, itemIndex);
},
),
OR if you want to scroll the page after the button is clicked then, you can use jumpTo() method using PageController which is clearly mentioned below by another user: #android.
Currently there's 2 options to handle your request:
PageView.builder(
controller: _pageController,
itemCount: _list.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_pageController.jumpToPage(index); // for regular jump
_pageController.animateToPage(_position, curve: Curves.decelerate, duration: Duration(milliseconds: 300)); // for animated jump. Requires a curve and a duration
},
child: Container();
);
}
),
You can use jumpTo() method to scroll position for PageView.
I have create one changePageViewPostion() method in below example:
import 'package:flutter/material.dart';
class MyPageView extends StatefulWidget {
createState() {
return StateKeeper();
}
}
class StateKeeper extends State<MyPageView> {
PageController controller = PageController(viewportFraction: 1, keepPage: true);
var currentPageValue = 0.0;
var mItemCount = 10;
#override
void initState() {
// TODO: implement initState
super.initState();
controller.addListener(() {
setState(() {
currentPageValue = controller.page;
});
});
}
void changePageViewPostion(int whichPage) {
if(controller != null){
whichPage = whichPage + 1; // because position will start from 0
double jumpPosition = MediaQuery.of(context).size.width / 2;
double orgPosition = MediaQuery.of(context).size.width / 2;
for(int i=0; i<mItemCount; i++){
controller.jumpTo(jumpPosition);
if(i==whichPage){
break;
}
jumpPosition = jumpPosition + orgPosition;
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PageView position change'),
),
body: PageView.builder(
controller: controller,
itemBuilder: (context, position) {
return Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Column(
children: <Widget>[
Center(
child: Text(
"Page " + (position + 1).toString(),
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(padding: EdgeInsets.only(bottom: 20),
child: FloatingActionButton(
elevation: 0.0,
child: new Icon(Icons.check),
backgroundColor: new Color(0xFFE57373),
onPressed: (){
changePageViewPostion(5);
}
),),
),
],
),
);
},
itemCount: mItemCount,
)
);
}
}
We can get current position with controller as below:
controller.addListener(() {
setState(() {
currentPageValue = controller.page.toInt();
print((currentPageValue + 1).toString());
});
});
Hope it helps :)
If you just want to scroll to the next page using a button, you can simply use the following method.
//Create a PageController variable
late PageController _pageController;
//Initialize the variable in the init method.
#override
void initState() {
_pageController = PageController(
initialPage: _activePage, keepPage: true, viewportFraction: 1);
super.initState();
}
//Use this nextPage() method in the onPressed() method.
onPressed: () {
setState(() {
_activePage < 2
? _activePage++
: Navigator.pushReplacementNamed(
context, LoginScreen.id);
});
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.decelerate,
);
}

Animating widget positions as the screen scrolls in flutter (GIF included)

I am trying to animate two Rows of widgets to collapse into 1 Row of these widgets as one scroll. I am trying to achieve this behavior inside a SliverAppBar.
For clarification, I have included a GIF here for reference. I would like the behavior you see in the app bar, but instead of 1 row to 2, I would like 2 rows becoming 1.
Here is a quick snippet of what I have so far. I wrapped 2 Row widgets that contain 3 shrinkableBox widgets each into a Wrap widget. I dynamically adjust the size of these boxes by hooking into _scrollController.offset and doing some calculations. The rows do move around dynamically but they don't animate and move abruptly instead.
double kExpandedHeight = 300.0;
Widget build(BuildContext context) {
double size = !_scrollController.hasClients || _scrollController.offset == 0 ? 75.0 : 75 - math.min(45.0, (45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5));
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: kExpandedHeight,
title: new Text(
"Title!",
),
bottom: PreferredSize(child: Wrap(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
],
),
],
), preferredSize: new Size.fromHeight(55),),
)
// ...
// ...Other sliver list content here...
// ...
You could use a Stack together with Positioned widgets to position the ShrinkableBoxes as you need. Since what controls the animation is the scroll offset, you don't need to use animated widgets or an animation controller or something like it. Here's a working example which calculates the positions by linearly interpolating the initial and final position of the boxes (you can get different animation paths by changing the Curves.linear to other curves):
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Home()));
}
class Home extends StatefulWidget {
#override
State createState() => HomeState();
}
class HomeState extends State<Home> {
static const double kExpandedHeight = 300.0;
static const double kInitialSize = 75.0;
static const double kFinalSize = 30.0;
static const List<Color> kBoxColors = [
Colors.red,
Colors.green,
Colors.yellow,
Colors.purple,
Colors.orange,
Colors.grey,
];
ScrollController _scrollController = new ScrollController();
#override
void initState() {
_scrollController.addListener(() {
setState(() { /* State being set is the Scroll Controller's offset */ });
});
}
#override
void dispose() {
_scrollController.dispose();
}
Widget build(BuildContext context) {
double size = !_scrollController.hasClients || _scrollController.offset == 0
? 75.0
: 75 -
math.min(45.0,
(45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5));
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: kExpandedHeight,
title: Text("Title!"),
bottom: PreferredSize(
preferredSize: Size.fromHeight(55),
child: buildAppBarBottom(size),
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
),
);
}
Widget buildAppBarBottom(double size) {
double t = (size - kInitialSize) / (kFinalSize - kInitialSize);
const double initialContainerHeight = 2 * kInitialSize;
const double finalContainerHeight = kFinalSize;
return Container(
height: lerpDouble(initialContainerHeight, finalContainerHeight, t),
child: LayoutBuilder(
builder: (context, constraints) {
List<Widget> stackChildren = [];
for (int i = 0; i < 6; i++) {
Offset offset = getInterpolatedOffset(i, constraints, t);
stackChildren.add(Positioned(
left: offset.dx,
top: offset.dy,
child: buildSizedBox(size, kBoxColors[i]),
));
}
return Stack(children: stackChildren);
},
),
);
}
Offset getInterpolatedOffset(int index, BoxConstraints constraints, double t) {
Curve curve = Curves.linear;
double curveT = curve.transform(t);
Offset a = getOffset(index, constraints, kInitialSize, 3);
Offset b = getOffset(index, constraints, kFinalSize, 6);
return Offset(
lerpDouble(a.dx, b.dx, curveT),
lerpDouble(a.dy, b.dy, curveT),
);
}
Offset getOffset(int index, BoxConstraints constraints, double size, int columns) {
int x = index % columns;
int y = index ~/ columns;
double horizontalMargin = (constraints.maxWidth - size * columns) / 2;
return Offset(horizontalMargin + x * size, y * size);
}
Widget buildSizedBox(double size, Color color) {
return Container(
height: size,
width: size,
color: color,
);
}
}

Flutter - How to have arbitrary number of implicit animations

I've made a widget that takes a list of children, and a List<double> of gaps, which displays the children with the respective gap between them. I've made it so passing a new list of gaps causes the widget to animate from the old gap to the new gaps (changing number of gaps not supported).
What's the best way to handle implicity animating the gaps?
This is the kind of behaviour I'm looking for:
(source: gfycat.com)
To avoid unneeded repetition you can move the tween logic to a custom widget.
You can also fuse List<Widget> children with List<double> gaps with a custom Gap widget.
Ultimately you can keep using ListView via separated constructor and use our custom Gap as separators.
Taking all of this into consideration, in the end your Gap widget is simply an AnimatedContainer with a custom height:
class Gap extends StatelessWidget {
final double gap;
const Gap(this.gap, {Key key})
: assert(gap >= .0),
super(key: key);
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
height: gap,
);
}
}
And you can then use it using the following:
ListView.separated(
itemCount: 42,
addAutomaticKeepAlives: true,
itemBuilder: (context, index) {
return RaisedButton(onPressed: null, child: Text("Foo $index"));
},
separatorBuilder: (context, index) {
return Gap(10.0);
},
),
The addAutomaticKeppAlives: true here is used to ensure that items leaving then reappearing don't have their animation reset. But it is not a necessity.
Here's a full example with dynamically changing gap size:
class Home extends StatefulWidget {
#override
HomeState createState() {
return new HomeState();
}
}
class HomeState extends State<Home> {
final rand = Random.secure();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
RaisedButton(
onPressed: () {
setState(() {});
},
child: Text("Reroll random gaps"),
),
Expanded(
child: ListView.separated(
addAutomaticKeepAlives: true,
itemCount: 42,
itemBuilder: (context, index) {
print("Bar");
return RaisedButton(onPressed: () {}, child: Text("Foo $index"));
},
separatorBuilder: (context, index) {
print("Foo $index");
return Gap(rand.nextDouble() * 10.0);
},
),
),
],
),
);
}
}
This was my solution, but my code is pretty messy. In particular, I'm not sure having a seperate list for the animations, tweens, controllers and curves (which is what I'm doing now) is the best way to do things. Also doing List<int>.generate(widget.gaps.length, (i) => i).forEach in the build function seems wrong, but the usual for (var i; i<x; i++) doesn't seem very dart-y either.
Is there a better way to handle these two issues?
class GappedList extends StatefulWidget {
final List<Widget> children;
final List<double> gaps;
GappedList({#required this.children, #required this.gaps}) :
assert(children != null),
assert(children.length > 0),
assert(gaps != null),
assert (gaps.length >= children.length - 1);
#override
GappedListState createState() {
return new GappedListState();
}
}
class GappedListState extends State<GappedList> with TickerProviderStateMixin{
List<Animation> _animations = [];
List<AnimationController> _controllers = [];
List<CurvedAnimation> _curves = [];
List<Tween<double>> _tweens;
#override
void initState() {
super.initState();
_tweens = widget.gaps.map((g) => Tween(
begin: g ?? 0.0,
end: g ?? 0.0,
)).toList();
_tweens.forEach((t) {
_controllers.add(AnimationController(
value: 1.0,
vsync: this,
duration: Duration(seconds: 1),
));
_curves.add(CurvedAnimation(parent: _controllers.last, curve: Curves.ease));
_animations.add(t.animate(_curves.last));
});
}
#override
void dispose() {
_controllers.forEach((c) => c.dispose());
super.dispose();
}
#override
void didUpdateWidget(GappedList oldWidget) {
super.didUpdateWidget(oldWidget);
assert(oldWidget.gaps.length == widget.gaps.length);
List<Tween<double>> oldTweens = _tweens;
List<int>.generate(widget.gaps.length, (i) => i).forEach(
(i) {
_tweens[i] = Tween<double>(
begin: oldTweens[i].evaluate(_curves[i]),
end: widget.gaps[i] ?? 0.0,
);
_animations[i] = _tweens[i].animate(_curves[i]);
if (_tweens[i].begin != _tweens[i].end) {
_controllers[i].forward(from: 0.0);
}
}
);
}
#override
Widget build(BuildContext context) {
List<Widget> list = [];
List<int>.generate(widget.children.length, (i) => i).forEach(
(i) {
list.add(widget.children[i]);
if (widget.children[i] != widget.children.last) {
list.add(
AnimatedBuilder(
animation: _animations[i],
builder: (context, _) => ConstrainedBox(
constraints: BoxConstraints.tightForFinite(
height: _animations[i].value,
),
),
)
);
}
}
);
return ListView(
primary: true,
shrinkWrap: true,
children: list,
);
}
}

Resources