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

Flutter中ListView长度变化时如何避免Item重新初始化

问题描述

我正在开发一款天气应用,从设备存储中获取城市列表,核心代码如下:

late List<String> citiesList;

@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: citiesList.length,
    itemBuilder: (context, index) => CityCard(
      name: citiesList[index], 
      dismissCallback: _deleteCity,
      key: ValueKey(citiesList[index])
    )
  );
}

Future<void> _deleteCity(String name) async {
  setState((){
    citiesList.remove(name);
  });
  await CitiesListManager().deleteCity(name: name);
}

每个CityCard会在初始化时调用API获取对应城市温度,加载完成前显示加载动画,CityCard代码如下:

class _CityCardState extends State {

  late Key key;
  late String cityName;
  late String temperature;
  late Future<double> temperatureFuture;
  late Function dismissCallback;

  _CityCardState({
    required String cityName, 
    required Function dismissCallback, 
    required Key key
  }){
    this.cityName = cityName;
    this.dismissCallback = dismissCallback;
    this.key = key;
  }

  @override
  void initState() {
    this.temperatureFuture = _getCurrentTemperature();
    super.initState();
  }

  Widget build(BuildContext context){
    return FutureBuilder(
      future: temperatureFuture,
      builder: (context, AsyncSnapshot<dynamic> snapshot){
        if (snapshot.hasData) {
          return Dismissible(
            key: key,
            onDismissed: (dismissDirection) async {
              await dismissCallback(cityName);
            },
            confirmDismiss: (dismissDirection) => _showConfirmDeletionDialog(context),
            child: Text(
              '$cityName',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: FontConstants.MIDDLE_SIZE
              ), 
            ),
          );
        } else {
          return Card();
        }
      }
    );
  }

  _showConfirmDeletionDialog(BuildContext dismissableContext){
    return showDialog<bool>(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: const Text('Confirm city deletion'),
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('Delete'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('Cancel'),
          ),
        ],
      ),
    ).then((value) {
      return value;
    });
  }

  Future<double> _getCurrentTemperature() async {
    final double currentTemperature = await WeatherApi().getCurrentTemperature(cityName);
    setState((){
      temperature = currentTemperature.toString();
    });
    return currentTemperature;
  }
}

当删除或添加城市时,所有CityCard都会重新显示加载动画,推测是列表项被重新初始化并再次请求API,请问如何避免这种情况?


解决方案

问题根源在于列表项的Key不稳定,且温度数据没有全局缓存,导致列表变化时大量组件重建并重复请求API。可以从以下三点优化:

1. 使用稳定唯一的Key

不要用城市名作为ValueKey的参数(重名城市会引发更严重的问题),建议给每个城市绑定唯一ID(比如存储时生成并记录),用ValueKey(cityId)作为列表项的Key,确保每个CityCard的标识始终稳定,让Flutter的组件复用机制能准确识别哪些项不需要重建。

如果暂时无法添加ID,也可以改用ObjectKey(citiesList[index]),但可靠性不如唯一ID。

2. 全局缓存温度数据

把温度缓存逻辑提到父组件或使用状态管理工具(比如Provider、Riverpod),避免每个CityCard单独请求API:

父组件添加缓存逻辑

late List<String> citiesList;
Map<String, double> _temperatureCache = {};

@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: citiesList.length,
    itemBuilder: (context, index) => CityCard(
      name: citiesList[index], 
      dismissCallback: _deleteCity,
      cachedTemperature: _temperatureCache[citiesList[index]],
      onTemperatureLoaded: (city, temp) {
        setState(() {
          _temperatureCache[city] = temp;
        });
      },
      key: ValueKey(citiesList[index]) // 建议替换为城市唯一ID
    )
  );
}

修改CityCard的状态逻辑

class CityCard extends StatefulWidget {
  final String name;
  final Function(String) dismissCallback;
  final double? cachedTemperature;
  final Function(String, double) onTemperatureLoaded;

  const CityCard({
    required this.name,
    required this.dismissCallback,
    this.cachedTemperature,
    required this.onTemperatureLoaded,
    required Key key,
  }) : super(key: key);

  @override
  _CityCardState createState() => _CityCardState();
}

class _CityCardState extends State<CityCard> {
  late Future<double>? _temperatureFuture;
  double? _currentTemperature;

  @override
  void initState() {
    super.initState();
    _currentTemperature = widget.cachedTemperature;
    // 只有缓存无数据时才发起API请求
    if (_currentTemperature == null) {
      _temperatureFuture = _getCurrentTemperature();
    }
  }

  @override
  void didUpdateWidget(covariant CityCard oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 同步父组件的缓存更新
    if (oldWidget.cachedTemperature != widget.cachedTemperature) {
      setState(() {
        _currentTemperature = widget.cachedTemperature;
      });
    }
  }

  Widget build(BuildContext context){
    // 已有缓存直接渲染卡片
    if (_currentTemperature != null) {
      return _buildCardWithTemperature();
    } 
    // 无缓存则显示FutureBuilder加载
    else if (_temperatureFuture != null) {
      return FutureBuilder(
        future: _temperatureFuture,
        builder: (context, AsyncSnapshot<double> snapshot){
          if (snapshot.hasData) {
            _currentTemperature = snapshot.data;
            widget.onTemperatureLoaded(widget.name, _currentTemperature!);
            return _buildCardWithTemperature();
          } else {
            return Card(); // 加载动画
          }
        }
      );
    } else {
      return Card();
    }
  }

  Widget _buildCardWithTemperature() {
    return Dismissible(
      key: widget.key!,
      onDismissed: (dismissDirection) async {
        await widget.dismissCallback(widget.name);
      },
      confirmDismiss: (dismissDirection) => _showConfirmDeletionDialog(context),
      child: Text(
        '${widget.name}: ${_currentTemperature!.toStringAsFixed(1)}°',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          fontSize: FontConstants.MIDDLE_SIZE
        ), 
      ),
    );
  }

  Future<double> _getCurrentTemperature() async {
    return await WeatherApi().getCurrentTemperature(widget.name);
  }

  Future<bool?> _showConfirmDeletionDialog(BuildContext context){
    return showDialog<bool>(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: const Text('确认删除城市'),
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('删除'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('取消'),
          ),
        ],
      ),
    );
  }
}

3. 优化列表更新方式

删除城市时,不要直接修改原列表,而是创建新列表触发setState,让Flutter的diff算法更准确识别变化:

Future<void> _deleteCity(String name) async {
  setState((){
    citiesList = List.from(citiesList)..remove(name);
    _temperatureCache.remove(name); // 可选:删除对应城市的缓存
  });
  await CitiesListManager().deleteCity(name: name);
}

通过以上优化,列表变化时只有新增或删除的项会触发必要操作,已加载的城市卡片不会重新初始化和显示加载动画。


内容的提问来源于stack exchange,提问作者Doomsdhg

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 03:45:52