Flutter ListView删除含FutureBuilder图片的列表项时图片错位问题
问题:Flutter ListView删除项后图片错位
我开发的Flutter应用中,ListView通过ListView.builder展示列表项,每个项包含描述和图片,数据从API获取,每个项由TestInternalWidget生成,图片用FutureBuilder加载(实际项目中fetchPhoto返回Uint8List,这里模拟返回URL)。
功能正常,但删除列表项时出现异常:项被移除后图片未正确更新。例如删除第2项后,对应描述消失,但第2项的图片会移到第3项的位置,以此类推。
删除第2项前:
删除第2项后:
我使用GetX进行状态管理,同时测试了标准setState配置,均存在此问题,恳请帮助解决。
代码文件
main.dart
import 'package:camera_app/views/widgets/testwidget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'controllers/testcontroller.dart'; void main() async { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { final TestController testController = Get.put(TestController()); return GetMaterialApp( title: 'test', home: Scaffold( appBar: AppBar( iconTheme: IconThemeData(color: Colors.black54), backgroundColor: Color(0xfffee6dc), centerTitle: true, ), backgroundColor: Color(0xfffff6f7), body: Obx(() => ListView.builder( cacheExtent: 100, addAutomaticKeepAlives: true, physics: BouncingScrollPhysics(), //physics : NeverScrollableScrollPhysics(), itemCount: testController.albumList.length, itemExtent: 120, itemBuilder: (context, index) { print("item build"); return InkWell( onDoubleTap: () { testController.albumList.removeAt(index); }, child: Obx(()=> TestInternalWidget(testController.albumList[index])) ); }) ) ) ); } }
testcontroller.dart
import 'dart:convert'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; //MODEL CLASS class Album { final int userId; final int id; final String title; String ? url; Album({ required this.userId, required this.id, required this.title, this.url }); factory Album.fromJson(Map<String, dynamic> json) { return Album( userId: json['userId'], id: json['id'], title: json['title'], ); } } List<Album> AlbumFromJson(String str) => List<Album>.from(json.decode(str).map((x) => Album.fromJson(x))); class TestController extends GetxController { var albumList = <Album>[].obs; @override void onInit() { print("init controller"); fetchItems(); super.onInit(); } Future<void> fetchItems() async { final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/')); List<Album> listAlbum; if (response.statusCode == 200) { listAlbum = AlbumFromJson(response.body); //Adding sample url for every item for(int i = 0; i< listAlbum.length; i++){ listAlbum[i].url = 'https://picsum.photos/250?image=' + i.toString(); } albumList.addAll(listAlbum); } else { throw Exception('Failed to load album'); } } }
testwidget.dart
import 'package:flutter/material.dart'; import '../../controllers/testcontroller.dart'; class TestInternalWidget extends StatefulWidget { Album album; TestInternalWidget(this.album); @override TestInternalWidgetState createState() => TestInternalWidgetState(); } class TestInternalWidgetState extends State<TestInternalWidget> { Future<String?> fetchPhoto() async { //here for test I'm returning only string url, but in real Uint8List object from my backend api return widget.album.url; } late Future<String?> myFuture; @override void initState() { myFuture = fetchPhoto(); super.initState(); } @override Widget build(BuildContext context) { print("internal widget build"); return Container( child:Column( children: [ FutureBuilder( future: myFuture, builder: (context, snapshot) { String url = snapshot.data as String; return snapshot.hasData && snapshot.data != null ? CircleAvatar( radius: 30.0, backgroundImage: NetworkImage(url), backgroundColor: Colors.transparent, ) : CircularProgressIndicator(); }, ), Text(widget.album.title), ], ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), color: Color(0xffE4CFC6) ) ); } }
问题原因
这个问题的核心是ListView的复用机制加上StatefulWidget的状态未更新导致的:
ListView.builder会复用列表项Widget,删除一个项后,后面的项会移到前面的位置,但对应的State对象不会重新创建,仍保留原来的状态。TestInternalWidget的myFuture在initState中初始化,而initState仅在Widget第一次创建时执行一次。复用Widget时,新的Album传递进来,但myFuture还是旧值,导致FutureBuilder展示旧图片。
解决办法
方式一:更新TestInternalWidget,在Widget更新时重新获取Future
重写didUpdateWidget方法,当传入的album变化时,重新初始化myFuture:
class TestInternalWidgetState extends State<TestInternalWidget> { Future<String?> fetchPhoto() async { return widget.album.url; } late Future<String?> myFuture; @override void initState() { myFuture = fetchPhoto(); super.initState(); } // 添加此方法 @override void didUpdateWidget(covariant TestInternalWidget oldWidget) { super.didUpdateWidget(oldWidget); // 当album对象变化时,重新获取future if (oldWidget.album != widget.album) { myFuture = fetchPhoto(); } } @override Widget build(BuildContext context) { // 原build代码保持不变 } }
方式二:给ListView项添加唯一Key,强制Widget重建
在ListView.builder的itemBuilder中,给每个TestInternalWidget设置唯一Key(比如用Album的id):
itemBuilder: (context, index) { print("item build"); final album = testController.albumList[index]; return InkWell( onDoubleTap: () { testController.albumList.removeAt(index); }, child: Obx(()=> TestInternalWidget(album, key: ValueKey(album.id))) // 添加Key ); }
同时修改TestInternalWidget的构造函数接收Key:
class TestInternalWidget extends StatefulWidget { final Album album; const TestInternalWidget(this.album, {super.key}); // 增加Key参数 @override TestInternalWidgetState createState() => TestInternalWidgetState(); }
两种方式都能解决图片错位问题,推荐方式一,因为方式二会强制重建Widget,可能带来轻微性能损耗;方式一仅更新Future,更高效。
内容的提问来源于stack exchange,提问作者pitcairn1987
相关产品推荐
相关产品推荐

