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

Flutter Getx添加第二行自适应TextField时报setState构建期错误

问题根因

报错由三个核心问题共同导致:

  • 在NewLineKeyword的build方法同步执行流程中,直接调用了会修改响应式变量的setNewLineSize方法。GetX响应式变量被修改时会立刻通知已挂载的GetX组件触发重建,而此时当前组件正处于构建阶段未完成挂载,直接触发了setState() or markNeedsBuild() called during build断言。第一行添加时无报错是因为首次构建时GetX响应式监听器还未完成绑定,修改状态不会触发重建通知,添加第二行时第一个组件的监听器已经处于工作状态,状态变更直接触发了构建阶段的重建标记。
  • 调用Get.put(NewLineKeywordController())时未指定tag,GetX默认按类型返回全局单例,所有NewLineKeyword组件实例实际共用同一个控制器,后续维护多行状态时极易出现状态串扰。
  • Editor组件中lineList的获取逻辑放在了GetX builder外部,后续_codeList更新时,builder内永远拿的是第一次构建时的旧列表,会出现新增行不显示的隐性问题。

修复方案
  1. 把build中直接修改状态的逻辑移到帧构建完成后的回调中执行,禁止在build同步流程中修改响应式变量。
  2. 给每个NewLineKeyword的控制器绑定唯一tag,保证每个输入框实例的控制器独立。
  3. 把Editor中lineList的获取逻辑移到GetX builder内部,保证响应式更新能拿到最新列表。
  4. 优化LineController的getKeywords逻辑,避免每次build都动态创建新组件实例触发无意义重建。

修改后的NewLineKeyword代码

class NewLineKeyword extends GetView<NewLineKeywordController>{
  final Key myKey;

  const NewLineKeyword({ 
    Key? key, 
    required this.myKey, 
  }) : super(key: key);

  // 用唯一key作为控制器tag,隔离不同实例的状态
  @override
  String? get tag => myKey.toString();

  @override
  Widget build(BuildContext context) {
    // put控制器时传入tag,绑定到当前组件实例
    final newLineKeywordController = Get.put(NewLineKeywordController(), tag: myKey.toString());

    // 初始化尺寸逻辑移到帧构建完成后执行,避免构建中触发状态更新
    WidgetsBinding.instance.addPostFrameCallback((_) {
      // 仅未初始化时赋值,减少不必要的更新
      if(newLineKeywordController.getNewLineSize(myKey) == 10.0){
        newLineKeywordController.setNewLineSize(myKey, 60.0);
      }
    });

    return GetX<NewLineKeywordController>(
      tag: myKey.toString(), // 指定tag找到对应实例的控制器
      builder: (_){
        return Row(
          children: <Widget>[
            const SizedBox(width: 7.5),
            SizedBox(
              width: newLineKeywordController.getNewLineSize(myKey),
              child: TextField(
                autofocus: true,
                minLines: 1,
                maxLines: 1,
                decoration: const InputDecoration(
                  constraints: BoxConstraints(minWidth: 15.0),
                  border: InputBorder.none,
                  contentPadding: EdgeInsets.only(bottom: 13.0),
                  focusedBorder: UnderlineInputBorder(
                    borderSide: BorderSide(color: Colors.blue)
                  )
                ),
                style: const TextStyle(color: Colors.white, fontSize: 20.0),
                onChanged: (String newValue){
                  final TextPainter textPainter = TextPainter(
                    text: TextSpan(text: newValue, style: const TextStyle(fontSize: 20.0)),
                    textDirection: TextDirection.ltr,
                    textScaleFactor: WidgetsBinding.instance.window.textScaleFactor,
                  )..layout();
                  newLineKeywordController.changeNewLineSize(myKey, textPainter.size.width + 2.0);
                },
                onSubmitted: (String newValue){
                  final TextPainter textPainter = TextPainter(
                    text: TextSpan(text: newValue, style: const TextStyle(fontSize: 20.0)),
                    textDirection: TextDirection.ltr,
                    textScaleFactor: WidgetsBinding.instance.window.textScaleFactor,
                  )..layout();
                  newLineKeywordController.changeNewLineSize(myKey, textPainter.size.width + 2.0);
                }
              )
            )
          ]
        );
      }
    );
  }
}

修改后的Editor代码

class Editor extends GetView<EditorController> {
  const Editor({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    ScrollController scrollCode = ScrollController();
    // 控制器只在组件初始化时put一次
    final editorController = Get.put(EditorController());

    return Padding(
      padding: const EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 120.0),
      child: GetX<EditorController>(
        builder: (_){
          // 列表取值移到builder内部,每次响应式更新都能拿到最新数据
          List<Line> lineList = editorController.getLineList();
          return DraggableScrollbar.arrows(
            controller: scrollCode,
            child: ListView.builder(
              controller: scrollCode,
              itemCount: lineList.length,
              itemBuilder: (BuildContext context, int index){
                return lineList[index];
              }
            )
          );
        }
      )
    );
  }
}

优化LineController的getKeywords方法

Keywords getKeywords(Key key){
  if(_mapKeywords[key] != null){
    return _mapKeywords[key]!;
  }
  else{
    // 找不到时返回空占位,不要动态创建新组件实例
    return const Keywords(reservedKeyword: "newLine", mykey: Key("empty"));
  }
}

注意事项
  • 所有需要在组件首次加载时执行的状态修改逻辑,禁止直接写在build方法的同步代码块中,可以放在addPostFrameCallback回调或者控制器的onInit生命周期中。
  • 如果多个同类型组件需要独立的GetX控制器,必须给Get.put和GetX组件指定唯一tag参数,否则GetX默认返回同类型的第一个单例对象,导致状态串扰。
  • 所有需要响应式更新的变量取值,必须放在GetX的builder函数内部,否则变量更新时builder不会重新执行,无法拿到最新值。

内容的提问来源于stack exchange,提问作者Héctor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 01:21:47