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

Flutter使用Freezed+Bloc时无法修改不可变列表问题求助

问题描述

修改列表元素时抛出错误:Can't modify list because of Unsupported operation: Cannot modify an unmodifiable list
技术栈:Bloc做状态管理,Freezed做代码生成,相关代码如下:

服务端获取角色绑定需求列表逻辑

Future<RoleWithNeeds> getRoleWithNeeds({
    required RoleWithNeeds selectedRoles,
  }) async {
    final dataFromApi = await _apiClient.fetchNeedsByRoles(selectedRoles.idRole);

    final needsApi = dataFromApi.data;
    final needsRepository = <Need>[];

    // convert to a repository model
    for (var i = 0; i < needsApi.length; i++) {
      needsRepository.add(Need(
        id: needsApi[i].id,
        name: needsApi[i].name,
        createdAt: needsApi[i].createdAt,
      ));
    }

    return selectedRoles.copyWith(needs: needsRepository);
  }

修改RoleWithNeeds嵌套数组逻辑

FutureOr<void> _onClickNeed(_ClickNeed event, Emitter<NeedState> emit) {
    emit(_Load());

    try {
      for (var role in _listRoleWithNeeds) {
        for (var need in role.needs) {
          if (need == event.need) {
            final changedNeed =
                event.need.copyWith(isSelected: !event.need.isSelected);
            _listRoleWithNeeds[_listRoleWithNeeds
                    .indexWhere((element) => element.needs.contains(need))]
                .needs[_listRoleWithNeeds[_listRoleWithNeeds
                    .indexWhere((element) => element.needs.contains(need))]
                .needs
                .indexOf(event.need)] = changedNeed;

            if (_searchedList.contains(event.need)) {
              _searchedList[_searchedList.indexOf(event.need)] = changedNeed;
            }

            break;
          }
        }
      }

      _selectedList.clear();
      for (final roleWithNeeds in _listRoleWithNeeds) {
        for (final need in roleWithNeeds.needs) {
          if (need.isSelected){
            _selectedList.add(need);
          }
        }
      }

      if(_isSelectedButtonPressed){
        emit(_Search(needs: _selectedList, sum: _selectedList.length));
      }
      else{
        !_isSearched
            ? emit(_Success(model: _listRoleWithNeeds, sum: _selectedList.length))
            : emit(_Search(needs: _searchedList, sum: _selectedList.length));
      }

    } catch (e) {
      emit(_Error(e.toString()));
    }
  }

模型定义

@freezed
class RoleWithNeeds with _$RoleWithNeeds {
  factory RoleWithNeeds({
    required int idRole,
    required String nameRole,
    required String svgUrl,
    @Default(true) bool isShort,
    @Default(<Need>[]) List<Need> needs,
    @Default(false) bool isSelected,
  }) = _RoleWithNeeds;

  factory RoleWithNeeds.fromJson(Map<String, dynamic> json) =>
      _$RoleWithNeedsFromJson(json);


}

@freezed
class Need with _$Need {
  factory Need({
    required int id,
    required String name,
    @Default(false) bool isSelected,
    String? createdAt,
  }) = _Need;

  factory Need.fromJson(Map<String, dynamic> json) => _$NeedFromJson(json);
}
错误原因

Freezed生成的数据类默认是不可变的,所有集合类型(List/Map/Set)属性会被自动包装为List.unmodifiable()返回的不可修改视图,不支持下标赋值、add、remove等修改操作。
现有代码直接通过多层下标索引修改role.needs的内部元素,本质是在修改不可变对象的只读属性,必然触发该异常。同时原有逻辑反复调用indexWhere查找元素下标,代码冗余且容易出现索引匹配错误。

修复方案

遵循Bloc和Freezed的不可变设计原则,所有属性修改都通过copyWith生成新的实例替换原有对象,不要直接修改原有实例的内部属性。
替换原有_onClickNeed方法中的列表修改逻辑为以下实现:

FutureOr<void> _onClickNeed(_ClickNeed event, Emitter<NeedState> emit) {
  emit(_Load());
  try {
    // 遍历生成新的角色列表,不修改原有对象
    final updatedRoleList = _listRoleWithNeeds.map((role) {
      // 查找当前角色下匹配的需求下标
      final targetNeedIndex = role.needs.indexWhere((item) => item == event.need);
      // 无匹配项直接返回原角色实例
      if (targetNeedIndex == -1) return role;
      // 复制生成新的可修改需求列表,替换目标项
      final updatedNeeds = List<Need>.from(role.needs);
      updatedNeeds[targetNeedIndex] = event.need.copyWith(isSelected: !event.need.isSelected);
      // 生成新的角色实例替换旧实例
      return role.copyWith(needs: updatedNeeds);
    }).toList();
    _listRoleWithNeeds = updatedRoleList;

    // 同步更新搜索列表
    final targetSearchedIndex = _searchedList.indexWhere((item) => item == event.need);
    if (targetSearchedIndex != -1) {
      final updatedSearchedList = List<Need>.from(_searchedList);
      updatedSearchedList[targetSearchedIndex] = event.need.copyWith(isSelected: !event.need.isSelected);
      _searchedList = updatedSearchedList;
    }

    // 重新计算选中列表
    _selectedList.clear();
    for (final roleWithNeeds in _listRoleWithNeeds) {
      for (final need in roleWithNeeds.needs) {
        if (need.isSelected) {
          _selectedList.add(need);
        }
      }
    }

    // 原有状态发射逻辑保持不变
    if (_isSelectedButtonPressed) {
      emit(_Search(needs: _selectedList, sum: _selectedList.length));
    } else {
      !_isSearched
          ? emit(_Success(model: _listRoleWithNeeds, sum: _selectedList.length))
          : emit(_Search(needs: _searchedList, sum: _selectedList.length));
    }
  } catch (e) {
    emit(_Error(e.toString()));
  }
}

如果确实需要直接修改集合属性(不推荐,会破坏状态可追踪性,和Bloc的不可变状态设计冲突),可以给对应集合属性添加@unfreezed注解,让Freezed不包装为不可修改列表:

@freezed
class RoleWithNeeds with _$RoleWithNeeds {
  factory RoleWithNeeds({
    required int idRole,
    required String nameRole,
    required String svgUrl,
    @Default(true) bool isShort,
    @unfreezed @Default(<Need>[]) List<Need> needs,
    @Default(false) bool isSelected,
  }) = _RoleWithNeeds;

  factory RoleWithNeeds.fromJson(Map<String, dynamic> json) =>
      _$RoleWithNeedsFromJson(json);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 00:18:20