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内永远拿的是第一次构建时的旧列表,会出现新增行不显示的隐性问题。
修复方案
- 把build中直接修改状态的逻辑移到帧构建完成后的回调中执行,禁止在build同步流程中修改响应式变量。
- 给每个
NewLineKeyword的控制器绑定唯一tag,保证每个输入框实例的控制器独立。 - 把
Editor中lineList的获取逻辑移到GetX builder内部,保证响应式更新能拿到最新列表。 - 优化
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
相关产品推荐
相关产品推荐

