HarmonyOS ArkUI:多窗口看板状态隔离与订阅释放(连载08)【共创稿事节】
工序排班台已经走到跨窗口协作这一步。前面几篇解决了容器换列、滚动锚点、时间轴映射、工位冲突、键盘编辑和异步回执乱序,但还有个看起来不起眼的问题:同一套排程数据被两个窗口打开,左边窗口选中焊接B02,右边窗口切到质检C03,为什么左边的选中框也跟着跑了?最初把所有状态塞进一个单例对象时,这种串联几乎不可避免。排程记录需要共享,页面焦点却不应该共享。
这里把应用中的窗口分成“主看板”和“对照看板”。两边都订阅修订43的工序计划;主看板选中B02、滚动锚点C03,对照看板选中C03、滚动锚点B02。修改排程以后两个看板更新工序时间,但各自的选中框、筛选条件和阅读位置维持独立。窗口关闭时,还要结束它登记的监听函数;下次打开时重新读取自己的视图偏好。这个划分没有依靠额外的系统级窗口同步接口,只要求窗口入口给页面提供稳定而不同的业务键。

先弄清哪些数据需要跨窗口传播
ShiftMatrix里的工序主数据仍然只有四条:A01机加08:00—09:20位于H-201,B02焊接09:20—12:00位于H-200,C03质检在修订43中调整为12:20—14:30位于H-202,D04包装14:10—18:00位于H-203。B02进行中,C03待排。两个窗口看见的是同一批工序和相同的修订号,但操作上下文不是同一份。把它们混在一起后,甚至会出现对照窗口滚动导致主窗口丢失当前位置的情况。
工程上可以按拥有者拆成两类。ScheduleHub持有经过回执修订校核的工序快照,只接受比当前修订号更新的结果;BoardViewState则记录某个窗口的selectedId、anchorId和筛选值。页面读取前者用于渲染记录,读取后者用于恢复自己的操作位置。筛选条件属于窗口层,不应写回排程模型。业务选中与键盘焦点也分别保留,避免打开第二个窗口时改变第一个窗口的操作目标。
以前的撤销操作会生成新修订,而不是覆盖旧数据。这个规则在多窗口里更重要:对照窗口如果刚收到修订42,主看板已经展示修订43,就不能因为回执晚到而倒退时间轴。修订号是共享域的先后条件,窗口键则是偏好域的命名空间,两者不要互相替代。
用订阅令牌管理共享排程
要解决的问题是让多个看板关注同一份排程,而不是让它们持有同一份UI状态。共享模块提供快照、版本门槛和显式取消订阅函数;订阅函数返回的解除句柄由创建它的组件自己保管。
// model/ScheduleHub.ets
export interface WorkStep {
id: string;
name: string;
status: string;
station: string;
start: number;
end: number;
}
export interface PlanSnapshot {
revision: number;
steps: WorkStep[];
}
export type PlanListener = (snapshot: PlanSnapshot) => void;
export class ScheduleHub {
private nextToken: number = 0;
private listeners: Map<number, PlanListener> = new Map();
private current: PlanSnapshot = { revision: 43, steps: [] };
snapshot(): PlanSnapshot {
return { revision: this.current.revision, steps: this.current.steps.slice() };
}
subscribe(listener: PlanListener): () => void {
const token: number = ++this.nextToken;
this.listeners.set(token, listener);
listener(this.snapshot());
return (): void => { this.listeners.delete(token); };
}
accept(reply: PlanSnapshot): boolean {
if (reply.revision <= this.current.revision) return false;
this.current = { revision: reply.revision, steps: reply.steps.slice() };
this.listeners.forEach((notify: PlanListener): void => notify(this.snapshot()));
return true;
}
listenerCount(): number { return this.listeners.size; }
}
export const scheduleHub: ScheduleHub = new ScheduleHub();
Map保存的是订阅令牌而不是窗口对象。取消订阅只删除自己的令牌,关闭对照窗口不会影响主看板。每次向订阅者传递浅拷贝数组,可以避免页面通过push直接改动共享数组;工序对象本身仍建议按不可变更新处理,若允许编辑嵌套字段,就需要在模型层进一步拷贝记录。这里也不把listenerCount当作系统窗口数量:它反映的是当前模块实例中注册的页面监听者数量。
同一进程中复用模块实例比较直接;如果后续需要跨进程协调,就应把共享工序存储换成有明确一致性策略的持久层或服务层,而不是默认一个单例能够跨进程传播。窗口打开顺序不会改变这个原则:初始快照可以为空,业务数据加载结束再推送最新修订;接收旧修订要拒绝,避免时间轴回退。
把视图偏好按窗口键保存
要解决的问题是页面重建后保留上次查看的工序,却不能把两个窗口写进同一个键。用户首选项适合保存少量配置:主看板用view:main-board,对照看板用view:compare-board。窗口键由应用入口分配,不拿对象地址、临时序号或屏幕宽度当标识。
// store/BoardViewPreference.ets
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
export interface BoardViewState {
selectedId: string;
anchorId: string;
filter: string;
}
export class BoardViewPreference {
private storage: preferences.Preferences;
constructor(context: common.UIAbilityContext) {
this.storage = preferences.getPreferencesSync(context, { name: 'board_views' });
}
private key(windowKey: string): string { return 'view:' + windowKey; }
load(windowKey: string): BoardViewState {
const fallback: BoardViewState = {
selectedId: 'B02', anchorId: 'C03', filter: '全部'
};
const raw: string = this.storage.getSync(this.key(windowKey), '') as string;
if (raw.length === 0) return fallback;
try {
const value: BoardViewState = JSON.parse(raw) as BoardViewState;
if (!value.selectedId || !value.anchorId || !value.filter) return fallback;
return value;
} catch (_) {
return fallback;
}
}
async save(windowKey: string, state: BoardViewState): Promise<void> {
this.storage.putSync(this.key(windowKey), JSON.stringify(state));
await this.storage.flush();
}
}
这里把flush()放在明确的保存动作后等待完成,而不是页面关闭瞬间再匆忙启动异步写入。偏好读取失败时返回合理缺省值,避免损坏的一条JSON阻断整个工序页。业务记录是否存在还需在实际列表到达后核对:例如C03从列表删除,滚动锚点就退回第一条可见工序。只有selectedId和anchorId都属于当前工序集合时,恢复动作才安全。持久化失败要保留本次内存状态并向用户提供可重试入口,不应吞掉异常后显示“已保存”。
偏好存储中的键只对应本地视图,不包含生产线内部敏感说明或完整排程。刷新共享数据时也无需频繁刷盘:筛选、选中和阅读位置在用户完成操作后合并保存,可以减少连续拖动手柄造成的写入次数。大体量工序明细不适合每次整体序列化到首选项,应交给原来的业务数据存储。

页面订阅谁创建,谁就负责释放
要解决的问题是窗口切换和组件销毁之后,旧回调仍然引用页面状态。下面的组件按windowKey恢复私有选中状态,订阅共享修订,在自定义组件离开时解除监听。主窗口可以传入main-board,对照窗口传入compare-board。真正创建第二个窗口的入口仍按照应用所使用的窗口模型配置,避免让业务组件承担窗口创建职责。
// pages/BoardPanel.ets
import { common } from '@kit.AbilityKit';
import { BoardViewPreference } from '../store/BoardViewPreference';
import { scheduleHub, PlanSnapshot } from '../model/ScheduleHub';
@Component
export struct BoardPanel {
@Prop windowKey: string = 'main-board';
@State selectedId: string = 'B02';
@State anchorId: string = 'C03';
@State revision: number = 43;
private preference?: BoardViewPreference;
private releaseListener?: () => void;
aboutToAppear(): void {
this.preference = new BoardViewPreference(getContext(this) as common.UIAbilityContext);
const local = this.preference.load(this.windowKey);
this.selectedId = local.selectedId;
this.anchorId = local.anchorId;
this.releaseListener = scheduleHub.subscribe((plan: PlanSnapshot): void => {
this.revision = plan.revision;
});
}
aboutToDisappear(): void {
this.releaseListener?.();
this.releaseListener = undefined;
}
private async selectStep(id: string): Promise<void> {
this.selectedId = id;
if (!this.preference) return;
try {
await this.preference.save(this.windowKey, {
selectedId: this.selectedId, anchorId: this.anchorId, filter: '全部'
});
} catch (_) {
console.error('保存视图偏好失败');
}
}
build() {
Column({ space: 12 }) {
Text(`窗口 ${this.windowKey} · 修订 ${this.revision}`)
Text(`选中 ${this.selectedId} · 锚点 ${this.anchorId}`)
Button('选中质检 C03').onClick(() => { this.selectStep('C03'); })
}.width('100%')
}
}
自定义组件的aboutToDisappear对应组件销毁阶段,不应在这里更新界面状态。解绑放在这里;保存则通过用户可见的操作提前触发。对于导航容器中只是被遮盖、尚未销毁的页面,还要结合具体导航生命周期决定是否暂停订阅,不能把“当前不可见”与“已经销毁”混为一谈。若页面再次创建,新的订阅令牌覆盖本次实例私有句柄,旧句柄应先结束。
在两个看板都显示时,预期共享模块会登记两个监听者;结束对照页面后只剩主看板一个。这个数量只是业务层预期值,适合作为订阅管理的检查点,不是系统窗口资源统计。更重要的检查是:对照窗口选择C03时,主看板仍然选中B02;新的修订到达时,两边的工序时间更新到同一版本。视图隔离和数据一致性同时满足,才算把所有权划清楚。

收尾时从一条操作链检查完整性
可以准备两组入口键按顺序走通场景:先打开主看板,选中B02并让C03位于阅读锚点;随后打开对照看板,选中C03但将B02作为锚点。给共享仓库送入修订44,只改变C03的排程时间,两个窗口都应看到新时间,彼此的选中状态却不应互相覆盖。关闭对照看板,检查取消订阅函数已经调用;再次打开它,应从view:compare-board恢复位置,而不是读取主窗口的保存值。
如果订阅函数无法释放,最直观的后果是导航次数增多之后同一次修订触发多份通知;如果每次拖动都刷盘,界面会无端承担持久化开销;如果把选中态写入共享排程,用户的局部操作就会污染其他窗口。三个问题的根源都是状态归属没有划清。这一节没有引入新的复杂页面框架,而是将七篇积累的操作状态按作用范围梳理清楚,让排班台在窗口生命周期变化时仍保留明确的恢复顺序。
相关API与生命周期说明可查阅华为开发者文档:ArkUI组件生命周期、用户首选项Preferences、UIAbilityContext与窗口阶段;不同窗口形态的适用条件应依据目标SDK的窗口开发文档核对。
更多推荐



所有评论(0)