I have laggy animation because of setState() in my code - flutter

I have animated pageview and listview that connected to each other. But I have an animation problem that caused by setState(i have tested to removed the setState and the animation works well).
import 'package:flutter/material.dart';
const double _listheight = 80;
class LoyaltyPage extends StatefulWidget {
#override
_LoyaltyPageState createState() => new _LoyaltyPageState();
}
class _LoyaltyPageState extends State<LoyaltyPage> {
PageController controller;
ScrollController listcontroller;
int selected = 0;
List<String> images=[
'https://images.pexels.com/photos/67636/rose-blue-flower-rose-blooms-67636.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
'https://images.pexels.com/photos/67636/rose-blue-flower-rose-blooms-67636.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
'https://images.pexels.com/photos/67636/rose-blue-flower-rose-blooms-67636.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
'https://images.pexels.com/photos/67636/rose-blue-flower-rose-blooms-67636.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
];
#override
initState() {
super.initState();
controller = new PageController(
initialPage: selected,
keepPage: false,
viewportFraction: 0.7,
);
listcontroller = new ScrollController();
}
#override
dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
iconTheme: IconThemeData(color: Colors.black),
title: Text(
'Loyalty',
style: TextStyle(color: Colors.black),
),
),
body: Column(
children: <Widget>[
Expanded(
flex: 3,
child: new Container(
child: new PageView.builder(
onPageChanged: (value) {
//ADDING THIS SET STATE CAUSE SELECTED COLOR WORKS BUT LAGGY ANIMATION
setState(() {
selected=value;
});
listcontroller.animateTo(value*_listheight,duration: Duration(milliseconds: 500),curve: Curves.ease);
},
itemCount: images.length,
controller: controller,
itemBuilder: (context, index) => builder(index)),
),
),
Expanded(
flex: 6,
child: new Container(
child: new ListView.builder(
controller: listcontroller,
itemCount: images.length,
itemBuilder: (context,index) => _listbuilder(index),
),
),
),
],
),
);
}
builder(int index) {
return new AnimatedBuilder(
animation: controller,
builder: (context, child) {
double value = 1.0;
if (controller.position.haveDimensions) {
value = controller.page - index;
value = (1 - (value.abs() * .4)).clamp(0.0, 1.0);
}
return new Center(
child: new SizedBox(
height: Curves.easeOut.transform(value) * 180,
width: Curves.easeOut.transform(value) * 270,
child: child,
),
);
},
child: new Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
semanticContainer: true,
clipBehavior: Clip.antiAliasWithSaveLayer,
child:Container(
child: Image.network(images[index],fit: BoxFit.fill,)
),
),
);
}
_listbuilder(int index){
return Container(
height: _listheight,
child: Column(
children: <Widget>[
ListTileTheme(
selectedColor: Colors.blueAccent,
child: ListTile(
title: Text(images[index],maxLines: 1,overflow: TextOverflow.ellipsis,),
subtitle: Text('Level : Gold'),
leading: GestureDetector(
onTap: (){
//ADDING THIS SET STATE CAUSE SELECTED COLOR WORKS BUT LAGGY ANIMATION
setState(() {
selected=index;
});
controller.animateToPage(index,duration: Duration(milliseconds: 500),curve: Curves.ease);
},
child: Icon(
Icons.credit_card),
),
trailing: Icon(Icons.navigate_next),
selected: index==selected,//THIS IS THE SELECTED THAT I TRIED TO CHANGE
),
),
Divider(height: 1,color: Colors.black45,),
],
),
);
}
}
I use setState at 2 places where it's on pagechanged and on leading listtile of my app.And here is my app looks like.
the first one is the expected output of my app but it laggy,
the second one has smooth animation but the text color doesn't change.

Try running your application in Release or Profile Mode.
There are performance-issues with animations in debug-mode when only a CircularProgressIndicator() is spinning. This is due to the fact that in Debug Mode, Dart gets compiled JIT instead of AOT. Thus, the Drawing Engine would need to compile 60 times / second while drawing the UI during the animation to run as smoothly as in AOT mode. This would take up a lot of resources as one might guess.
As long as you don't call setState({}) on the AnimationController..addListener() you should be fine with your specific implementation. Please refer to this article for further details on Animations and setState({}): https://medium.com/flutter-community/flutter-laggy-animations-how-not-to-setstate-f2dd9873b8fc .

Related

The swap widget so that the under widget has been fixed

I have a create so simple slidable view pager with CarouselSlider:
return Scaffold(
body: CarouselSlider(
options: CarouselOptions(
viewportFraction: 1,
// aspectRatio: 1,
height: double.maxFinite,
// enlargeCenterPage: true,
),
items: List.generate(
10,
(i) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
color: (i % 2 == 0) ? Colors.red : Colors.green,
),
),
Text('text $i', style: TextStyle(fontSize: 16.0)),
],
)),
));
This is its result:
But as you can see next container connects to the first widget, I want when the first widget to be swapped to the left, the next widget appears under the first widget Not next to it. It looks like the following widget is fixed and we remove the top widget.
You can use a package called stacked_page_view, it is very simple, lightweight, and similar to the same original PageView in usage.
Example Snippet:
PageView.builder(
itemCount: 10,
scrollDirection: Axis.vertical,
controller: pageController,
itemBuilder: (context, index) {
return StackPageView(
controller: pageController,
index: index,
child: Container(
color: (colors..shuffle()).first,
child: Center(
child: Text(
'$index',
style: TextStyle(
color: Colors.white,
fontSize: 25,
),
),
),
),
);
},
)
Note: You can control the scroll axis with the property scrollDirection inside PageView.builder() with values of Axis.vertical or Axis.horizontal.
I finally find a way to create stack page view, This is a full codes:
import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/material.dart';
import 'dummy_data.dart';
import 'page_view_item.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
/// The current page of the page view
double _page = 0;
/// The index of the leftmost element of the list to be displayed
int get _firstItemIndex => _page.toInt();
/// Controller to get the current position of the page view
final _controller = PageController(
viewportFraction: 0.5,
);
/// The width of a single item
late final _itemWidth =
MediaQuery.of(context).size.width * _controller.viewportFraction;
#override
void initState() {
super.initState();
_controller.addListener(() => setState(() {
_page = _controller.page!;
}));
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("LV Scroll"),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Positioned.fill(
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: _itemWidth,
child: FractionallySizedBox(
child: PageViewItem(
index: _firstItemIndex,
width: _itemWidth,
url: model[_firstItemIndex],
),
),
),
),
),
SizedBox(
height: 250,
child: PageView.builder(
padEnds: false,
controller: _controller,
itemBuilder: (context, index) {
return Opacity(
opacity: index <= _firstItemIndex ? 0 : 1,
child: PageViewItem(
index: index,
width: _itemWidth,
url: model[index],
),
);
},
itemCount: model.length,
),
),
],
),
],
),
);
}
}
it's result :
and its reference;
You can use a package called expandable_page_view, it is a PageView widget adjusting its height to currently displayed page. It accepts the same parameters as classic PageView.
ExpandablePageView.builder(
itemCount: 3,
itemBuilder: (context, index) {
return Container(color: Colors.blue);
},
),

Is there a listener that can call a function to move on to next set of data in Carousel (Page View)?

I've set up this Carousel using a PageView.builder. It displays 5 tiles at a time.
Once the user has swiped all the way over to the right & pulls on the last tile (see image)...I'd like to move onto the next set of 5 tiles in an array.
Is there an event handler for this? I've managed to set up a listener that can determine when the user has swiped to the last tile, but cannot figure out how to tell when they're pulling on this so it can be refreshed.
Appreciate any help I can get on this. Code below :)
import 'package:flutter/material.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
class RecommendationPanel extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _buildRecommendationPanel();
}
}
class _buildRecommendationPanel extends State<RecommendationPanel> {
PageController _pageController = PageController();
#override
void initState() {
_pageController = PageController(viewportFraction: 1.0);
_pageController.addListener(_scrollListener);
super.initState();
}
void dispose() {
_pageController.dispose();
super.dispose();
}
_scrollListener() {
if (_pageController.offset >= _pageController.position.maxScrollExtent &&
!_pageController.position.outOfRange) {
setState(() {
//This is working in the sense that it tells when they're on the final tile
//I want it so knows when you drag to the right
print('Final tile');
//I could refresh the list and then just move everything back to #1 in the view...i.e. the last card [index 4] can now shift to 5
//_pageController.jumpToPage(0);
});
}
if (_pageController.offset <= _pageController.position.minScrollExtent &&
!_pageController.position.outOfRange) {
setState(() {
//Need to figure out how to work this - there's going to have to be another variable checking what place in the top N recommended array it is, and then adjust accordingly
print('Back to first tile');
//_pageController.jumpToPage(3);
});
}
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
//You may want to use aspect ratio here for tablet support
height: 270.0,
child: PageView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: 5,
scrollDirection: Axis.horizontal,
controller: _pageController,
itemBuilder: (BuildContext context, int itemIndex) {
//I could potentially call something here to update the slider index
return _buildCarouselItem(context, itemIndex);
},
),
),
Container(
height: 30,
child: Center(
child: SmoothPageIndicator(
controller: _pageController,
count: 5,
effect: WormEffect(
spacing: 8.0,
dotHeight: 10,
dotWidth: 10,
activeDotColor: Colors.orange,
),
),
),
),
],
);
}
Widget _buildCarouselItem(BuildContext context, int itemIndex) {
List<String> labels = [
'Pub',
'Bar',
'Football Match',
'Nightclub',
'Book Festival',
'Six',
'Seven',
'Eight',
'Nine',
'Ten',
];
return Padding(
padding: EdgeInsets.symmetric(horizontal: 2.0),
child: Container(
height: double.infinity,
//color: Colors.red,
child: Column(
children: [
Container(
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
child: Container(
// In here is where I should build each individual tile
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Container(
width: double.infinity,
height: 260,
child: Text(labels[itemIndex]),
),
),
),
),
],
),
),
);
}
Widget buildIndicator(BuildContext context, int itemIndex, int count) {
return AnimatedSmoothIndicator(
activeIndex: itemIndex,
count: count,
effect: WormEffect(
dotColor: Colors.grey,
activeDotColor: Colors.orange,
),
);
}
}

How to get rid of overflow-error on AnimatedContainer?

I implemented a MaterialBanner. I created a slide-up-effect once the user pushes the dismiss-button. Everything works ok, except for the overflow-error 'bottom overflowed by .. pixels', which appears when you click the dismiss button. The number of pixels in the error message counts down to zero as the bottom slides up. How can I solve this last issue? I expected the MaterialBanner to respect the maxHeight of the BoxConstraint instead of overflowing.
AnimatedContainer buildAnimatedBanner(AuthViewModel vm) {
return AnimatedContainer(
constraints: BoxConstraints(maxHeight: _heightBanner),
duration: Duration(seconds: 3),
child: MaterialBanner(
backgroundColor: Colors.green,
leading: Icon(EvilIcons.bell,
size: 28, color: AppTheme.appTheme().colorScheme.onBackground),
content: Text('Please check your inbox to verify email ${vm.email}'),
actions: <Widget>[
FlatButton(
child: Text(
"Send again",
style: TextStyle(
color: AppTheme.appTheme().colorScheme.onBackground),
),
onPressed: () {},
),
FlatButton(
child: Text(
"Dismiss",
style: TextStyle(
color: AppTheme.appTheme().colorScheme.onBackground),
),
onPressed: () => setState(() => _heightBanner = 0),
),
],
),
);
}
It's overflowing because while the container is reducing in height, the content of the banner is still being rendered in full. You'll still see this error for as long as the material banner is still in view.
Looking at your code, I'm thinking the purpose of the AnimatedContainer is to make a smooth transition when the height of the child (MaterialBanner) changes.
You can use an AnimatedSize instead. It'll automatically handle the size change transitions for you and you don't have to worry about Overflow error.
It also provides an alignment param you can use to determine the direction of the transition.
Below is a code. Demo can be found in this codepen.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: MyApp(),
),
);
}
class MyApp extends StatefulWidget {
MyWidget createState() => MyWidget();
}
class MyWidget extends State<MyApp> with SingleTickerProviderStateMixin {
int itemCount = 2;
bool pressed = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedSize(
vsync: this,
duration: Duration(milliseconds: 500),
alignment: Alignment.topCenter,
child: Container(
color: Colors.red,
// height: 300,
width: 300,
child: ListView.builder(
itemCount: itemCount,
shrinkWrap: true,
itemBuilder: (context, index) {
return ListTile(
title: Text("$index"),
);
}
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState((){
itemCount = pressed ? 6 : 4;
pressed = !pressed;
});
}
),
);
}
}

ScrollablePositionedList with SliverAppBar not working properly

This is a repository to create a minimal reproducible example.
I want SliverAppBar hidden when ScrollablePositionedList.builder is Scrolled. This is the relevant piece of code I am including here.
NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
backgroundColor: Colors.blue,
expandedHeight: 112,
snap: true,
pinned: false,
floating: true,
forceElevated: true,
actions: <Widget>[
IconButton(
icon: Icon(Icons.event),
)
],
flexibleSpace: SafeArea(
child: Column(
children: <Widget>[
Container(
height: kToolbarHeight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Title',
style: Theme.of(context)
.textTheme
.title
.copyWith(
fontSize: 16, color: Colors.white),
),
SizedBox(
height: 2,
),
Text(
'Date',
style: Theme.of(context)
.textTheme
.caption
.copyWith(
fontSize: 10, color: Colors.white),
),
SizedBox(
height: 2,
),
Text(
'Another Text',
style: Theme.of(context)
.textTheme
.subtitle
.copyWith(
fontSize: 14, color: Colors.white),
),
],
),
),
Expanded(
child: Container(
height: kToolbarHeight,
width: MediaQuery.of(context).size.width,
color: Colors.white,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text(
'Prev',
),
Text(
'Next',
)
],
),
),
)
],
),
),
)
],
body: ScrollablePositionedList.builder(
physics: ScrollPhysics(),
itemPositionsListener: itemPositionListener,
itemScrollController: _itemScrollController,
initialScrollIndex: 0,
itemCount: 500,
itemBuilder: (BuildContext ctxt, int index) {
return Container(
margin: EdgeInsets.all(16)
,
child: Text('$index'));
})),
I tried two approaches so far none of them working properly,
Approach 1
I added physics: ScrollPhysics(), to ScrollablePositionedList.builder
Output:
Appraoch 2
I added physics: NeverScrollableScrollPhysics(), to ScrollablePositionedList.builder
SliverAppBar hides this time but now I can not scroll to the very end of ScrollablePositionedList.builder I have 500 items on my list but it scrolls up to only 14th item, see the output. Also, it lags too much on scroll
Output:
Thanks in advance.
Answering question myself
This problem has no solution for it. I have created an issue here
It looks like ScrollablePositionedList with SliverAppBar cannot work until Flutter Team does not add shrinkwrap property to ScrollablePositionedList.
Feature request to add shrinkwrap is created here
It works for me
//create list of global keys
List<GlobalKey> _formKeys = [];
//assign keys from your list
for(int i=0 ;i< syourlist.length;i++){
final key = GlobalKey();
_formKeys.add(key);
}
//in list view give key as below
key:_formKeys[index]
//on button click
Scrollable.ensureVisible(_formKeys[index].currentContext);
Here is a basic workaround:
Use the ItemsPositionsListener to listen for the current item the list has scrolled to.
Then create boolean values to check the scroll-direction and amount.
These conditions control an AnimatedContainer controlling the height of a custom header.
This is placed as a child in a Column with the header in a Flexible widget so the scrollablelist correctly takes up the space before and after animation.
Although this is very basic and does not use the NestedScrollView, it keeps use of the ScrollablePositionedList, and achieves a similar effect with a header that slides in and out, based on the set scroll conditions.
Providing in case helps anyone else, until the underlying issue is fixed...:)
import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class ScrollAllWords extends StatefulWidget {
const ScrollAllWords({
Key? key,
required this.list,
}) : super(key: key);
final List<String> list;
#override
State<ScrollAllWords> createState() => _ScrollAllWordsState();
}
class _ScrollAllWordsState extends State<ScrollAllWords> {
/// use this listener to control the header position.
final _itemPositionsListener = ItemPositionsListener.create();
///Can also use the ItemScrollController to animate through the list (code omitted)
final _itemScrollController = ItemScrollController();
/// Gets the current index the list has scrolled to.
int _currentIndex = 0;
/// Compares against current index to determine the scroll direction.
int _shadowIndex = 0;
bool _reverseScrolling = false;
bool _showHeader = true;
#override
void initState() {
/// Set up the listener.
_itemPositionsListener.itemPositions.addListener(() {
checkScroll();
});
super.initState();
}
void checkScroll() {
/// Gets the current index of the scroll.
_currentIndex =
_itemPositionsListener.itemPositions.value
.elementAt(0)
.index;
/// Checks the scroll direction.
if (_currentIndex > _shadowIndex) {
_reverseScrolling = false;
_shadowIndex = _currentIndex;
}
if (_currentIndex < _shadowIndex) {
_reverseScrolling = true;
_shadowIndex = _currentIndex;
}
/// Checks whether to show or hide the scroller (e.g. show when scrolled passed 15 items and not reversing).
if (!_reverseScrolling && _currentIndex > 15) {
_showHeader = false;
} else {
_showHeader = true;
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 120),
height: _showHeader ? 200 : 0,
curve: Curves.easeOutCubic,
child: Container(
color: Colors.red,
height: size.height * 0.20,
),
),
Flexible(
child: ScrollablePositionedList.builder(
itemScrollController: _itemScrollController,
itemPositionsListener: _itemPositionsListener,
itemCount: widget.list.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.list[index]),
);
},
),
),
],
);
}
}

Flutter Error: 'indexOf(child) > index': is not true. (StreamBuilder,PageView)

I'm trying to create a screen that is contained within a pageview, that also contains a page view for part of the screen.
To acheive this I have an unlimited page view for the whole page itself, then every page has a header view, with a bottom half that has a page view with 3 possible options. I have this pretty much working, however, the pages I am using I would like a StreamBuilder... This is where the issue is caused.
class DiaryPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _DiaryPage();
}
class _DiaryPage extends State<DiaryPage> with TickerProviderStateMixin {
DiaryBloc _diaryBloc;
TabController _tabController;
PageController _pageController;
#override
void initState() {
_diaryBloc = BlocProvider.of<DiaryBloc>(context);
_diaryBloc.init();
_tabController = TabController(length: 3, vsync: this);
_pageController = PageController(initialPage: _diaryBloc.initialPage);
super.initState();
}
#override
void dispose() {
_diaryBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Flexible(
child: PageView.builder(
controller: _pageController,
itemBuilder: (BuildContext context, int position) {
return _buildPage(_diaryBloc.getDateFromPosition(position));
},
itemCount: _diaryBloc.amountOfPages,
),
);
}
Widget _buildPage(DateTime date) {
return Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[_getHeader(date), _getTabBody()],
);
}
Widget _getHeader(DateTime date) {
return Card(
child: SizedBox(
width: double.infinity,
height: 125,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(8, 16, 8, 0),
child: Text(
'${DateFormat('EEEE').format(date)} ${date.day} ${DateFormat('MMMM').format(date)}',
style: Theme.of(context).textTheme.subtitle,
textScaleFactor: 1,
textAlign: TextAlign.center,
),
),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => {
_pageController.previousPage(
duration: Duration(milliseconds: 250),
curve: Curves.ease)
},
),
const Expanded(child: LinearProgressIndicator()),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () => {
_pageController.nextPage(
duration: Duration(milliseconds: 250),
curve: Curves.ease)
},
),
],
),
Container(
height: 40.0,
child: DefaultTabController(
length: 3,
child: Scaffold(
backgroundColor: Colors.white,
appBar: TabBar(
controller: _tabController,
unselectedLabelColor: Colors.grey[500],
labelColor: Theme.of(context).primaryColor,
tabs: const <Widget>[
Tab(icon: Icon(Icons.pie_chart)),
Tab(icon: Icon(Icons.fastfood)),
Tab(icon: Icon(Icons.directions_run)),
],
),
),
),
),
],
),
),
);
}
Widget _getTabBody() {
return Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
_getOverviewScreen(),
_getFoodScreen(),
_getExerciseScreen(),
],
),
);
}
// TODO - this seems to be the issue, wtf and why
Widget _getBody() {
return Flexible(
child: StreamBuilder<Widget>(
stream: _diaryBloc.widgetStream,
initialData: _diaryBloc.buildEmptyWidget(),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
return snapshot.data;
},
),
);
}
Widget _getExerciseScreen() {
return Text("Exercise Screen"); //_getBody();
}
Widget _getFoodScreen() {
return Text("Food Screen"); //_getBody();
}
Widget _getOverviewScreen() {
return _getBody();
}
}
As you can see, there are three widgets being returned as part of the sub page view, 2 of them are Text Widgets which show correctly, but the StreamBuilder, which is populated correctly with another Text Widget seems to give me the red screen of death. Any ideas?
Fixed the problem, it was related to the StreamBuilder being wrapped in a Flexible rather than a column. I then added column to have a mainAxisSize of max... Seemed to work.
For custom ListView/PageView
In my case, I wanted to clear the list of my listview. In a custom ListView/PageView, the findChildIndexCallback will find the element's index after i.e. a reordering operation, but also when you clear the list.
yourList.indexWhere()unfortunately returns -1 when it couldn't find an element. So, Make sure to return null in that case, to tell the callback that the child doesn't exist anymore.
...
findChildIndexCallback: (Key key) {
final ValueKey<String> valueKey = key as ValueKey<String>;
final data = valueKey.value;
final index = images.indexWhere((element) => element.id == data);
//important here:
if (index > 0 ) return index;
else return null;
},