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

使用Flutter GetX更新Observable List时触发StackOverflow错误

解决GetX中更新Protocol列表触发StackOverflow错误的问题

问题场景

开发一个允许用户回答多个protocol(如睡眠时长、疼痛)的页面,从Rest API获取protocols列表,当用户完成某个protocol的回答后,尝试更新列表中对应项时触发StackOverflow错误。

相关代码

GetxController代码

@override
onInit() async {
  await fetchProtocol();
  tabController = TabController(length: protocols.length, vsync: this);
  loading.value = false;
  super.onInit();
}

Future<void> fetchProtocol() async {
  try {
    protocols.value = await repository.fetchProtocol(protocolId!);
  } catch (e) {
    showErrorDialog(
        dimissible: false,
        onClicked: () =>
            Get.offAllNamed(Routes.training, arguments: trainignId),
        message: "Parece que você não pode responder a este protocolo.");
  }
}
updateProtocol(Protocol newProtocol) {
  protocols.value = replaceProtocolById(protocols, newProtocol);
}

View代码

Widget build(BuildContext context) {
  return WillPopScope(
      onWillPop: () async => true,
      child: Scaffold(
          bottomNavigationBar: const SizedBox.shrink(),
          appBar: PreferredSize(
              preferredSize: const Size.fromHeight(50),
              child: AppBar(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.vertical(
                      bottom: const Radius.circular(5).r,
                    ),
                  ),
                  title: Text(controller.title ?? "",
                      maxLines: 1,
                      style: TextStyle(
                          color: Colors.white,
                          fontSize: 22.sp,
                          fontWeight: FontWeight.normal)),
                  centerTitle: true,
                  automaticallyImplyLeading: false,
                  leading: null),
            ),
            body: SafeArea(
              child: Container(
                  width: Get.width,
                  height: Get.height,
                  alignment: Alignment.topCenter,
                  child: SizedBox(
                    width: 342.w,
                    child: Obx(() {
                      if (controller.loading.value) {
                        return Loading();
                      } else {
                        return TabBarView(
                            controller: controller.tabController,
                            children: List.generate(controller.protocols.length,
                                (index) {
                              if (controller.protocols[index].id == 4) {
                                return PainProtocol(
                                  onFeel: controller.changeViewedProtocol,
                                  onNotFeel: controller.changeViewedProtocol,
                                  position: controller.position.value,
                                  show: controller.viewedProtocol.value,
                                  onConfirmPains: controller.updateProtocol,
                                  protocol: controller.protocols.value[index],
                                );
                              } else {
                                return Center(
                                  child: Text(
                                      controller.protocols.value[index].name!),
                                );
                              }
                            }));
                      }
                    }),
                  )),
            )));
}

Protocol类代码

class Protocol {
  int? id;
  String? name;
  String? nickname;
  String? measure;
  int? order;
  Value? value;
  List<Indicator>? indicator;
  String? type;

  Protocol(
      {this.id,
      this.name,
      this.nickname,
      this.measure,
      this.order,
      this.value,
      this.indicator,
      this.type});
}

问题原因

核心问题出在updateProtocol方法:

  • 直接将可观察列表protocols传入replaceProtocolById,如果该方法内部操作原列表或未生成新列表实例,赋值给protocols.value时会触发Obx重建
  • Obx重建时重新生成TabBarView的所有子widget,每个子widget都会读取protocols.value,进而再次触发列表更新,形成无限循环,最终导致StackOverflow

解决方案

1. 实现正确的replaceProtocolById方法,生成新列表实例

确保方法返回全新的列表,不修改原可观察列表:

List<Protocol> replaceProtocolById(List<Protocol> protocols, Protocol newProtocol) {
  return protocols.map((protocol) {
    return protocol.id == newProtocol.id ? newProtocol : protocol;
  }).toList();
}

2. 修复updateProtocol方法,传入列表快照

调用replaceProtocolById时传入protocols.value(当前列表的快照),避免直接操作可观察对象:

void updateProtocol(Protocol newProtocol) {
  protocols.value = replaceProtocolById(protocols.value, newProtocol);
}

3. 动态更新TabController(可选优化)

监听protocols变化,确保TabController长度与列表同步,避免重建时出现异常:

@override
void onInit() async {
  await fetchProtocol();
  tabController = TabController(length: protocols.length, vsync: this);
  
  // 监听列表变化,更新TabController
  protocols.listen((newProtocols) {
    if (tabController.length != newProtocols.length) {
      tabController.dispose();
      tabController = TabController(length: newProtocols.length, vsync: this);
    }
  });
  
  loading.value = false;
  super.onInit();
}

4. 确保Protocol实例不可变

更新Protocol时生成新实例,而非修改原有对象,让GetX正确检测到变化:

// 示例:创建更新后的Protocol实例
Protocol updatedProtocol = Protocol(
  id: existingProtocol.id,
  name: existingProtocol.name,
  value: updatedValue, // 替换为用户提交的新值
  nickname: existingProtocol.nickname,
  measure: existingProtocol.measure,
  order: existingProtocol.order,
  indicator: existingProtocol.indicator,
  type: existingProtocol.type,
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 14:53:22