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

Flutter中如何在不重建已有组件的前提下追加子组件?

解决方案:保留选中状态的动态日志流展示

针对你遇到的「新日志追加导致文本选中状态丢失」的问题,这里提供两种实用方案,兼顾功能需求和开发效率:

方案一:用EditableText模拟可选中富文本(推荐)

SelectableText本质是基于EditableText实现的,直接使用EditableText可以更灵活地控制文本更新和选中状态,避免重建导致的状态丢失。

实现步骤:

  1. 用StatefulWidget封装组件,持有TextEditingController和FocusNode来管理文本和焦点
  2. 初始化时加载历史日志,监听日志流追加新内容
  3. 更新文本前保存选中状态,更新后自动恢复
class LogDisplay extends StatefulWidget {
  final LogManager logManager;

  const LogDisplay({super.key, required this.logManager});

  @override
  State<LogDisplay> createState() => _LogDisplayState();
}

class _LogDisplayState extends State<LogDisplay> {
  final _controller = TextEditingController();
  final _focusNode = FocusNode();
  TextSelection? _lastSelection;
  late TextSpan _textSpan;

  @override
  void initState() {
    super.initState();
    // 加载历史日志
    _updateTextSpan(widget.logManager.currentLogs);
    // 监听新日志流
    widget.logManager.logStream.listen((newLog) {
      setState(() {
        // 保存当前选中状态
        _lastSelection = _controller.selection;
        // 追加新日志的TextSpan
        final newSpan = _buildLogSpan(newLog);
        _textSpan = TextSpan(children: [..._textSpan.children!, newSpan]);
        // 更新controller的文本内容(用于选中逻辑)
        final newText = _controller.text + newSpan.text!;
        _controller.value = TextEditingValue(text: newText);
      });
      // 恢复选中状态
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (_lastSelection != null) {
          _controller.selection = _lastSelection!;
          _lastSelection = null;
        }
      });
    });
  }

  // 构建单条日志的TextSpan
  TextSpan _buildLogSpan(LogEntry log) {
    return TextSpan(
      text: '${log.timestamp}: ${log.message}\n',
      style: TextStyle(
        color: log.level == LogLevel.error ? Colors.red : Colors.black,
        fontSize: 14,
      ),
    );
  }

  // 初始化历史日志的TextSpan
  void _updateTextSpan(List<LogEntry> logs) {
    final spans = logs.map(_buildLogSpan).toList();
    _textSpan = TextSpan(children: spans);
    _controller.text = spans.map((s) => s.text).join();
  }

  @override
  Widget build(BuildContext context) {
    return EditableText(
      controller: _controller,
      focusNode: _focusNode,
      readOnly: true, // 禁止编辑,仅保留选中功能
      textSpan: _textSpan,
      style: const TextStyle(color: Colors.black),
      cursorColor: Colors.transparent, // 隐藏光标
      contextMenuBuilder: (context, editableTextState) {
        // 自定义上下文菜单,只保留复制功能
        return AdaptiveTextSelectionToolbar(
          anchors: editableTextState.contextMenuAnchors,
          children: [
            CopyTextSelectionToolbarItem(
              onPressed: () {
                editableTextState.copySelection(SelectionChangedCause.toolbar);
                Navigator.pop(context);
              },
            ),
          ],
        );
      },
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    _focusNode.dispose();
    super.dispose();
  }
}

方案二:保存并恢复SelectableText的选中状态

如果你坚持使用SelectableText.rich,可以通过GlobalKey获取组件状态,在文本更新前后手动保存和恢复选中状态:

class LogDisplay extends StatefulWidget {
  final AsyncValue<List<LogEntry>> logs;

  const LogDisplay({super.key, required this.logs});

  @override
  State<LogDisplay> createState() => _LogDisplayState();
}

class _LogDisplayState extends State<LogDisplay> {
  final _selectableKey = GlobalKey<SelectableTextState>();
  TextSelection? _savedSelection;

  @override
  void didUpdateWidget(covariant LogDisplay oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 更新前保存选中状态
    _savedSelection = _selectableKey.currentState?.currentSelection;
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 更新后恢复选中状态
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_savedSelection != null) {
        _selectableKey.currentState?.updateSelection(
          _savedSelection!,
          cause: SelectionChangedCause.textUpdate,
        );
        _savedSelection = null;
      }
    });
  }

  TextSpan _buildFullTextSpan(List<LogEntry> logs) {
    return TextSpan(
      children: logs.map((log) => TextSpan(
        text: '${log.timestamp}: ${log.message}\n',
        style: TextStyle(color: log.level == LogLevel.error ? Colors.red : Colors.black),
      )).toList(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return widget.logs.when(
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => Text('加载失败:$e'),
      data: (logs) => SelectableText.rich(
        key: _selectableKey,
        _buildFullTextSpan(logs),
        style: const TextStyle(fontSize: 14),
      ),
    );
  }
}

注意事项

  • 方案一的优势是无需频繁重建整个文本组件,仅追加新内容,性能更优,且选中状态恢复更稳定
  • 方案二需要注意:当文本长度变化较大时,原有选中位置可能出现偏移(比如选中末尾时,追加新日志后选中状态仍停留在原末尾),这是符合预期的行为

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 17:14:56