You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的状态未更新导致的:

  1. ListView.builder会复用列表项Widget,删除一个项后,后面的项会移到前面的位置,但对应的State对象不会重新创建,仍保留原来的状态。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 09:25:28