New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AnimatedList calls initState of children below a removed/added item, disallowing smooth transitions. #63185
Comments
Hi @SwissCheese5 |
@TahaTesser Reproducing the bug is proving a little tricky because my project is quite complex, however, while making an isolated attempt, I ran into another interesting issue. I don't know if this issue is related at all to this one or not, nevertheless, I would love some insight and to have a better understanding of how AnimatedList works. class ListItem extends StatefulWidget {
const ListItem({
Key key,
this.index,
this.remove,
this.animation,
}) : super(key: key);
final int index;
final Function remove;
final Animation<double> animation;
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
Future<Uint8List> bytes;
String url;
@override
void initState() {
print('initState @ #${widget.index}');
url = 'https://picsum.photos/seed/${widget.index}/200/300';
super.initState();
}
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: widget.animation,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
widget.remove();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: MediaQuery.of(context).size.width,
height: 100,
color: Colors.primaries[widget.index % Colors.primaries.length],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.network('$url'),
Text('item #${widget.index}'),
Image.network('https://picsum.photos/seed/${widget.index}/200/300'),
],
)),
),
),
));
}
} You can see the images on the LEFT are treating the index as if it's the list's index position rather than the true index content. The images on the RIGHT are not doing this. I am incredibly confused as to why this is happening. Each item's internal number index should never change, i have no idea what's going on. What am I missing here? Here's the whole code that you can copy paste and get the app running directly: import 'dart:typed_data';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var list = List.generate(10, (index) => index);
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bug #63185',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
body: SafeArea(
child: AnimatedList(
key: _listKey,
initialItemCount: list.length,
itemBuilder: (BuildContext context, int index, Animation<double> animation) {
var i = list[index];
return ListItem(
index: i,
animation: animation,
remove: () {
_listKey.currentState.removeItem(index, (context, animation) {
return ListItem(index: i, animation: animation);
});
list.removeAt(index);
},
);
}),
)),
);
}
}
class ListItem extends StatefulWidget {
const ListItem({
Key key,
this.index,
this.remove,
this.animation,
}) : super(key: key);
final int index;
final Function remove;
final Animation<double> animation;
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
Future<Uint8List> bytes;
String url;
@override
void initState() {
print('initState @ #${widget.index}');
url = 'https://picsum.photos/seed/${widget.index}/200/300';
super.initState();
}
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: widget.animation,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
widget.remove();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: MediaQuery.of(context).size.width,
height: 100,
color: Colors.primaries[widget.index % Colors.primaries.length],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.network('$url'),
Text('item #${widget.index}'),
Image.network('https://picsum.photos/seed/${widget.index}/200/300'),
],
)),
),
),
));
}
} |
In the example provided here https://api.flutter.dev/flutter/widgets/AnimatedList-class.html |
Code Sampleimport 'dart:typed_data';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var list = List.generate(10, (index) => index);
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bug #63185',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
body: SafeArea(
child: AnimatedList(
key: _listKey,
initialItemCount: list.length,
itemBuilder:
(BuildContext context, int index, Animation<double> animation) {
var i = list[index];
return ListItem(
index: i,
animation: animation,
remove: () {
_listKey.currentState.removeItem(index, (context, animation) {
return ListItem(index: i, animation: animation);
});
list.removeAt(index);
},
);
}),
)),
);
}
}
class ListItem extends StatefulWidget {
const ListItem({
Key key,
this.index,
this.remove,
this.animation,
}) : super(key: key);
final int index;
final Function remove;
final Animation<double> animation;
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
Future<Uint8List> bytes;
String url;
@override
void initState() {
print('initState @ #${widget.index}');
url = 'https://picsum.photos/seed/${widget.index}/200/300';
super.initState();
}
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: widget.animation,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
widget.remove();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: MediaQuery.of(context).size.width,
height: 100,
color:
Colors.primaries[widget.index % Colors.primaries.length],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image.network('$url'),
Text('item #${widget.index}'),
Image.network(
'https://picsum.photos/seed/${widget.index}/200/300'),
],
)),
),
),
));
}
}
flutter doctor -v
|
Would like to point out that this makes AnimatedList completely useless under most circumstances that have to do with a widget being Stateful. I would mark this as severe. |
The problem appears to go away when you key the ListItems. In the code sample in #63185 (comment) replace the ListTile with the follwoing: return ListItem(
key: ValueKey(i), // <-- NEW
index: i,
animation: animation,
remove: () {
_listKey.currentState.removeItem(index, (context, animation) {
return ListItem(index: i, animation: animation);
});
list.removeAt(index);
},
); |
@goderbauer This issue isn't done yet. We solved part of the problem maybe, but there's still the thumbnail flickering because initState is getting called on them again for no reason. I wrapped my TileMusic widget with a Container holding the value key to be extra positive that the widget contains a key at the top. AnimatedList(
key: _listKey,
initialItemCount: tracks.length,
itemBuilder: (BuildContext context, int index, animation) {
var track = tracks[index];
return Container(
key: ValueKey(track.id),
child: TileMusic(
track,
trackController,
animation,
onHeart: () {
if (user.trackIDs.contains(track.id)) {
tracks.removeAt(index);
_listKey.currentState.removeItem(
index,
(context, animation) =>
TileMusic(track, trackController, animation));
} else {
_listKey.currentState.insertItem(index);
}
},
onTapBody: () {
// ignore: close_sinks
var player = BlocProvider.of<PlayerBloc>(context);
var trackState = BlocProvider.of<TrackCubit>(context).state;
bool isThisPlaying = trackState.activeTrack == track;
if (isThisPlaying) {
player.add(PlayerEventStopped());
} else {
player.add(PlayerEventPlay(track));
}
updateUI(context);
},
),
);
},
), Still not sure why it calls init state again. It calls initState immediately on TileMusic for all children below the removed item. |
Any news on this one? |
Nope, gave up and moved on |
Is the existing reproducible example sufficient? This is still a problem with
The following code also reproduces the problem, maybe it is of help:
|
This issue is reproducible on the latest DemoScreen.Recording.2023-04-27.at.17.58.23.movflutter doctor -v (stable and master)[✓] Flutter (Channel stable, 3.7.12, on macOS 13.0.1 22A400 darwin-x64, locale en-VN)
• Flutter version 3.7.12 on channel stable at /Users/huynq/Documents/GitHub/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 4d9e56e694 (2 days ago), 2023-04-17 21:47:46 -0400
• Engine revision 1a65d409c7
• Dart version 2.19.6
• DevTools version 2.20.1
[✓] Android toolchain - develop for Android devices (Android SDK version 32.0.0)
• Android SDK at /Users/huynq/Library/Android/sdk
• Platform android-33, build-tools 32.0.0
• ANDROID_HOME = /Users/huynq/Library/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 14.3)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 14E222b
• CocoaPods version 1.11.3
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2022.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
[✓] IntelliJ IDEA Community Edition (version 2022.1.1)
• IntelliJ at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5591.52/IntelliJ IDEA CE.app
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
[✓] VS Code (version 1.77.3)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.62.0
[✓] Connected device (3 available)
• SM T225 (mobile) • R9JT3004VRJ • android-arm64 • Android 13 (API 33)
• macOS (desktop) • macos • darwin-x64 • macOS 13.0.1 22A400 darwin-x64
• Chrome (web) • chrome • web-javascript • Google Chrome 112.0.5615.137
[✓] HTTP Host Availability
• All required HTTP hosts are available
• No issues found! [!] Flutter (Channel master, 3.10.0-15.0.pre.18, on macOS 13.0.1 22A400 darwin-x64, locale en-VN)
• Flutter version 3.10.0-15.0.pre.18 on channel master at /Users/huynq/Documents/GitHub/flutter_master
! Warning: `flutter` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/flutter, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path.
! Warning: `dart` on your path resolves to /Users/huynq/Documents/GitHub/flutter/bin/dart, which is not inside your current Flutter SDK checkout at /Users/huynq/Documents/GitHub/flutter_master. Consider adding /Users/huynq/Documents/GitHub/flutter_master/bin to the front of your path.
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision e454644dd5 (33 minutes ago), 2023-04-26 21:32:34 -0400
• Engine revision d4ca5240da
• Dart version 3.1.0 (build 3.1.0-47.0.dev)
• DevTools version 2.23.1
• If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.
[✓] Android toolchain - develop for Android devices (Android SDK version 32.0.0)
• Android SDK at /Users/huynq/Library/Android/sdk
• Platform android-33, build-tools 32.0.0
• ANDROID_HOME = /Users/huynq/Library/Android/sdk
• Java binary at: /Applications/Android Studio Flamingo.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 14.3)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 14E222b
• CocoaPods version 1.11.3
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2022.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
[✓] Android Studio (version 2022.2)
• Android Studio at /Applications/Android Studio Flamingo.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)
[✓] IntelliJ IDEA Community Edition (version 2022.1.1)
• IntelliJ at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5591.52/IntelliJ IDEA CE.app
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
[✓] VS Code (version 1.77.3)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.62.0
[✓] Connected device (2 available)
• macOS (desktop) • macos • darwin-x64 • macOS 13.0.1 22A400 darwin-x64
• Chrome (web) • chrome • web-javascript • Google Chrome 112.0.5615.137
[✓] Network resources
• All expected network resources are available.
! Doctor found issues in 1 category. |
Any update? |
This should be a major concern as there is no way to preserve the state of the widgets under edited item index or there should be a guide on the flutter document, staying that the |
Is this Open from 2020? Really? I think the animated list is one of the most useful widgets in flutter? Why it's taking so long? @TahaTesser Do you think, is this even possible to fix this issue, or we need to rewrite the animated list widget? |
@payam-zahedi |
Thanks Taha |
Any updates in here? |
Steps to Reproduce
When I used AnimatedList and remove an item from the list, it calls initState on all the children below the removed item, which causes a widget refresh.
Here's a gif representing the problem:
You can see, when each item is removed, the thumbnails under all the children reload. This is not the expected behavior. The expected behavior is for initState NOT to get called again, and disallowing another widget refresh.
What you see in the gif is my best attempt at pulling the thumbnails as fast as I can. Normally, i get the image file, check if it exists, then read its bytes for every image. I sped it up by caching the bytes into a HiveBox and pulling that data back as fast as possible in initState if it was loaded before.
Here is my initState for the images:
You can see I have both a method that resolves the image manually, stores it in hive, and then if initState is called again, it get the data from HiveBox instead for speed.
And this is how I'm building the widget:
You can see I've completely eliminated the chance of a FutureBuilder and a TweenAnimationBuilder from being the source of the error, because if the image is reloaded again, the widget tree changes to DIRECTLY draw the Image.memory widget. And yet, a flash still persists. You can also see my attempts are using ValueKeys so flutter know it's the same widget.
This issue is therefore less about loading my image instantly, and more about preventing initState from being called twice for no reason.
Of course, the error could be on my part and I'm just adding the thumbnail widget incorrectly to my AnimatedList, so here's that code:
removeItem() is called by the onTap of the heart icon.
Im not sure what I'm doing wrong for all the children to refresh, or if this is a bug or not.
Logs
The text was updated successfully, but these errors were encountered: