鸿蒙 ArkUI 文件树实战:树状态丢失、懒加载断链定位与列表滚动边界(ArkTS 状态管理 V2)

文件树是编辑器的"门面":打开文件夹、浏览、新建、重命名、删除,看着都是 CRUD。但 MarkPin 的文件树前后攻坚了三轮(三个批次、十几个缺陷与增强),踩的坑集中在三个领域:树状态管理、懒加载后的定位、ArkUI 列表组件的硬边界。这篇按批次讲,代码全部来自真实源码。

摘要:本文记录 MarkPin 文件树三轮攻坚的完整过程。批次一解决整树重建导致展开状态丢失的架构问题,通过持久化树结构 + 懒加载 + 增量重建显示列表修复;批次二解决懒加载断链导致"定位"静默失败的问题,采用按 URI 反解路径逐段补链重试;批次三应对 ArkUI 列表横竖滚动互斥的硬边界,用"恒定外包"方案绕行。全文附真实源码,并总结能力边界与三条实战心得。

一、批次一:整树重建丢状态——架构级返工

现象

初版文件树有个致命缺陷:点开任何一个目录都展不开。点击后闪一下,目录还是收着的。

分析定位

排查链路:点击目录 → 展开处理 → 刷新树。问题出在最后一步的实现方式——展开后做了整树重建(重新从根节点拉取并构建整棵树),而重建出来的新节点对象上,"是否展开"这个字段回到了默认值。也就是说:状态存在节点对象上,重建把对象换掉了,状态自然丢失。更糟的是整树重建还顺带毁掉了两个体验:滚动位置重置、选中高亮丢失。

修复代码

三个动作组合:树结构持久化为组件成员(不再从根重建);展开只做懒加载子节点 + 翻转标志 + 增量重建显示列表;节点类用状态管理框架的深度观测装饰器标注,保证标志翻转能驱动 UI 刷新:

// entry/src/main/ets/components/FileTreePanel.ets(真实代码,节选)
@ObservedV2
export class FileTreeNode {
  name: string = '';
  fullPath: string = '';
  uri: string = '';
  isDirectory: boolean = false;
  level: number = 0;
  @Trace isExpanded: boolean = false;      // 深度观测:翻转即驱动 UI
  @Trace children: Array<FileTreeNode> = [];
}

private toggleExpand(node: FileTreeNode): void {
  if (!node.isDirectory) return;
  const before: boolean = node.isExpanded;
  if (!before) {
    // 展开前先加载子节点(懒加载;空目录每次展开重读,便于发现新增文件)
    if (node.children.length === 0) {
      this.loadDirectoryChildren(node);
    }
    node.isExpanded = true;
  } else {
    node.isExpanded = false;
  }
  const root = this.rootNode;
  if (root !== undefined) {
    this.rebuildDisplayList(root);   // 只重建扁平显示列表,不重建树结构
  }
}

配套还有一个细节:列表渲染的 key 从"路径 + 下标"改成纯完整路径——用下标做 key 时,展开/折叠会让下标漂移,框架按 key 比对后误判"整列表都变了",触发整列表重建。key 的稳定性就是列表的身份稳定性

批次一还顺带定了几个规矩:目录在前文件在后、按名称不区分大小写排序;所有类型文件可见(含 . 开头隐藏项,与主流编辑器对齐);非 Markdown 文件点击后打开"占位标签"(显示信息不可编辑,避免二进制文件读进编辑器);可编辑类型的判定收敛到单一事实源函数,文件树图标、点击行为都读它,未来加新类型只改一处。

二、批次二:懒加载断链——"定位"为什么总是失败

现象

增强需求:切换标签时,文件树自动定位并高亮当前文件(在折叠目录里就自动展开)。实现后实测点切换不定位,手动折叠目录偶尔也失效。

分析定位

三个根因叠加,都很典型:

  1. 静默失败:定位算法沿树找路径,树是懒加载的——祖先还没加载时 children 是空的,查找失败后直接返回,连滚动都不发生,用户看到的就是"没反应";
  2. 时序竞争:定位后同步调用滚动接口,但此时重建的显示列表还没完成渲染布局,滚动动画基于旧布局计算目标位置;
  3. 首屏盲区:应用启动恢复会话时直接设置当前文件,不经过"变化通知",监听器不触发——首屏永不定位。

修复代码

针对根因一,查找失败时按 URI 反解路径,逐段同步补链后重试——懒加载断链就现场把链补上:

// entry/src/main/ets/components/FileTreePanel.ets(真实代码,节选)
private revealSelectedFileInTree(): void {
  if (this.selectedFileUri.length === 0) return;
  let rootNode = this.rootNode;
  if (rootNode === undefined) return;

  // §3.12 根因A修复:懒加载断链时按 URI 反解路径逐段同步补链后重试
  let path: Array<FileTreeNode> = [];
  if (!this.collectPathToUri(rootNode, this.selectedFileUri, path)) {
    if (!this.expandChainByPathPrefix(this.selectedFileUri)) {
      return;
    }
    rootNode = this.rootNode;
    if (rootNode === undefined) return;
    path = [];
    if (!this.collectPathToUri(rootNode, this.selectedFileUri, path)) {
      return;
    }
  }
  // 展开祖先链 → 重建显示列表 → 定位下标 → 延帧滚动(见下)
}

针对根因二,滚动前强制等待一帧(延帧滚动),让动画基于新布局计算;针对根因三,首屏渲染完成后补一次主动定位。修复后折叠目录里的文件切换标签也能逐级展开、平滑滚到位。这一批的通用教训:懒加载结构的"查找"必须回答"链断了怎么办"——答案是补链重试,而不是返回失败

批次二还包含新建体验的重构:新建目标目录按优先级解析(选中目录 → 当前文件所在目录 → 根目录),同名冲突同步预检(存在就提示,弹窗保留可改名重试——顺带修掉了"同名文件被静默打开旧文件"的暗缺陷),根目录作为树的第一行可折叠。新建文件夹不自动展开(空目录展开没意义),新建文件自动展开目标目录并打开标签。

三、批次三:ArkUI 列表的硬边界

第三批是体验层:侧栏宽度拖拽、面板切换保状态、超长文件名横向滚动。前两个是常规活(拖拽范围做了"最小值 + 主区域百分比 + 绝对上限"双重钳制,持久化保存;文件树⇄大纲切换从"销毁重建"改为"双挂载显隐切换",展开态与滚动位置全保留)。真正有意思的是第三个——ArkUI 的列表组件不支持横竖双向同时滚动,而超长文件名需要横向滚动。

绕行方案是"恒定外包":在列表外无条件包一层水平滚动容器(注意是无条件——条件包裹会在切换时触发组件树重建,又回到丢状态的老问题),靠宽度差产生横向滚动;数据侧先估算全树最大行宽(缩进 + 图标区 + 名称字符宽,全角/半角分别计量),内容不超宽时滚动条自动不出现。纵向虚拟化不受影响。

四、验证与效果

三个批次两轮模拟器验证全过:树展开/折叠/多级展开、新建/重命名/删除后的展开状态保留、切换标签自动定位(含深层折叠目录)、首屏定位、超长文件名横滚与纵向滚动共存、宽度拖拽钳制与持久化。删除断链目录时的回退(定位目标消失自动清空选中)也一并覆盖。

五、能力边界表

事项AI 表现我的结论
树状态架构(持久成员 + 懒加载 + 增量列表)重构方案一次到位"状态跟着对象走"前提下,对象身份必须稳定
深度观测装饰器的必要性初版遗漏,UI 不刷新后补上状态框架的观测边界要显式标注,不能依赖默认
定位断链的补链方案方案正确,初版有静默失败懒加载结构的查找必须设计"断链路径"
ArkUI 横竖滚动互斥给出条件包裹方案(有坑),修正为恒定外包平台硬边界面前,方案要过"会不会重建组件树"的审
key 稳定性初版用路径+下标,自踩坑列表 key 的黄金法则:身份不变的元素 key 不变

六、三条心得

  1. 树组件的状态架构先于功能:展开态、选中态、滚动位置存哪、重建时跟不跟着走,这三个问题不想清楚,功能越多欠账越多;
  2. 懒加载的代价是"一切按需"都要准备断链:查找、定位、统计,每个依赖完整树的逻辑都需要断链预案;
  3. 平台组件的硬边界要查官方文档确认:列表横竖滚动互斥这类约束,早查一步就少绕一轮——我们最终靠官方 FAQ 确认了好几个 API 语义(新建目录/判存在也是)。

如果你在做 ArkUI 或树组件,或者想看 MarkPin 后续,关注专栏。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐