1. 前言
随身手记最基本的承诺,是刚写下的话下次还在。这个收尾练习就用一段本地手记,把页面布局与部署配置接到一次真正的读写里:编辑、保存、换窗口大小,再结束进程重新打开,看看刚才的文字是否回来。
这篇把工程配置与运行表现分别确认。示例做成一页随身手记,核心操作是编辑、保存和重启后读取。宽窗口增加设备信息区,紧凑窗口专心编辑,让同一份业务代码在不同空间里保持清楚的使用方式。
2. 手机与二合一部署声明
先检查包是否声明了目标设备。模块的 deviceTypes 使用 phone 和 2in1,分别对应手机与二合一设备类别。配置中需要的是平台定义的值;产品名称、显示屏尺寸和窗口宽度都不能替代这两个条目。部署声明明确之后,再检查同一份手记页怎样布局、怎样保存。
下面是 entry/src/main/module.json5 的完整配置。页面入口、资源引用和模块信息沿用 Empty Ability 的基本结构,设备类型明确列在同一个模块中。
json
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
}
}
工程使用 HarmonyOS 6.1.1、API 24。能够安装还取决于目标设备系统版本、模块类型和包的其他要求,deviceTypes 表达的是支持范围,编译与实际安装仍然需要分别验证。
纯 ArkTS 页面也不等于可以忽略部署差异。以后加入原生库、特殊系统能力或设备专属交互,应继续确认对应设备与架构的支持情况。把安装声明、编译、运行和业务验证逐项对齐,比仅看到一个欢迎页面更可靠。
配置文件最好与实际包一起检查。入口页面、模块名、资源引用和设备类型共同组成可部署的工程,单独复制一项 deviceTypes 并不会自动替其他条件兜底。当前已经用二合一模拟器完成读写和宽窄窗口验证,手机端继续使用同一页面实现,但安装与输入体验仍应在对应环境另行核对。
3. Preferences写入与落盘
只显示设备名称,比较难判断页面能不能正常使用。因此,这里保留一项实际业务:在 TextArea 中编辑一段文字,点击保存后写入当前设备的 Preferences,重新启动应用再读取。
读取使用固定的首选项名称 journal 与键 note。第一次打开时使用默认手记,已有内容时显示本机保存的值。获取存储实例是异步过程,因此保存方法先检查实例是否就绪,再执行写入,不提前显示"保存成功"。
写入时先 put,再等待 flush 完成,随后更新反馈;出现异常则显示保存失败。put 对应首选项中的值更新,flush 将待保存的数据写入持久化文件,这两个步骤在示例里明确接在一起。
每台设备保存自己的手记。以后把同一个应用安装到另一台设备,它会使用那台设备自己的应用数据;如果产品需要跨设备同步,还应继续设计账号、同步服务或其他数据协同机制。

继续接入真实业务时,还需要检查空内容、较长文字、读取失败与存储失败。本例已给出未就绪和异常反馈,但 Preferences 仍更适合轻量数据;如果手记发展成大量记录、复杂搜索与分类,就应该重新评估数据模型与存储方式。
3.1 手记编辑区与窗口代码
信息区显示 deviceInfo.deviceType,帮助确认当前运行环境;是否显示这一块区域,则按页面内容宽度是否达到 780 vp 判断。二合一设备上的窄窗口,也会收起辅助信息,将空间留给编辑器。
这里没有按"设备属于电脑"就始终固定两栏。真实用户可以把应用摆在屏幕一角,也可以同时打开多个窗口。只看设备类别而忽略当前空间,会让一个本来能正常运行的应用,仍然在小窗口里显得拥挤。
宽屏信息区使用固定比例,编辑区分配剩余空间。文字编辑状态一直保存在同一个 note 中,显示或收起辅助区域不会重新初始化它。窗口尺寸变化与本地存储也分开处理,不会每拖一下边缘就写一次首选项文件。
尺寸按钮用于二合一自由窗口中的重复观察。手机上的窗口形态由系统管理,窗口操作可能受到限制,方法保留相应失败反馈。手记的读取、编辑和保存不依赖这些演示按钮。
尺寸回调只安排信息区是否出现,保存方法只处理当前文本。两者分开后,拖动窗口不会制造额外写盘,恢复宽窗也不用重新加载默认文字。手记的生命周期由数据读写管理,版面只改变它在眼前如何展开,这也是这个小业务适合用于检查多端页面的原因。
pages/Index.ets:
typescript
import { LengthMetrics } from '@kit.ArkUI';
import { deviceInfo } from '@kit.BasicServicesKit';
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { resizeWindow } from '../common/WindowTools';
@Entry
@Component
struct Index {
@State private viewportWidth: number = 0;
@State private note: string = '今天也有值得记住的小事。';
@State private status: string = '正在读取本机手记';
private store?: preferences.Preferences;
async aboutToAppear(): Promise<void> {
try {
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
this.store = await preferences.getPreferences(context, { name: 'journal' });
this.note = await this.store.get('note', this.note) as string;
this.status = '已读取本机手记';
} catch (error) { this.status = '读取失败,请稍后重试'; }
}
private async save(): Promise<void> {
if (!this.store) { this.status = '存储尚未就绪';return; }
try {
this.getUIContext().getFocusController().clearFocus();
await this.store.put('note', this.note);
await this.store.flush();
this.status = '已保存在当前设备';
} catch (error) { this.status = '保存失败,请重试'; }
}
private async resize(w: number): Promise<void> {
this.status = await resizeWindow(this.getUIContext().getHostContext() as common.UIAbilityContext, w, this.getUIContext().vp2px(1));
}
@Builder
information() {
Column({ space: 20 }) {
Text('一份代码,随处记下').fontSize(28).fontWeight(FontWeight.Bold).fontColor('#F7F4E7').width('100%');
Text('支持 phone / 2in1').fontSize(17).fontColor('#D7E5CE').width('100%');
Text(`当前设备类型:${deviceInfo.deviceType}`).fontSize(15).fontColor('#D7E5CE').id('deviceType').width('100%');
Text(`内容宽度:${Math.round(this.viewportWidth)} vp`).fontSize(15).fontColor('#D7E5CE').id('viewport').width('100%');
Text('设备类型决定安装范围,窗口宽度决定当下排版。').fontSize(17).lineHeight(28).fontColor('#F7F4E7').width('100%');
}.padding(28).width('100%').height('100%').backgroundColor('#587456').borderRadius(24);
}
@Builder
editor() {
Column({ space: 20 }) {
Text('随身手记').fontSize(30).fontWeight(FontWeight.Bold).fontColor('#3E4A38').width('100%');
TextArea({ text: this.note, placeholder: '记下一件小事' }).id('note').layoutWeight(1).width('100%')
.fontSize(20).backgroundColor('#FCFAF1').onChange((value: string) => { this.note = value; });
Flex({ wrap: FlexWrap.Wrap, space: { main: LengthMetrics.vp(8), cross: LengthMetrics.vp(8) } }) {
Button('保存手记').id('save').backgroundColor('#587456').onClick(() => this.save());
Button('紧凑窗口').id('narrow').onClick(() => this.resize(450));
Button('展开窗口').id('wide').onClick(() => this.resize(1080));
}.width('100%');
Text(this.status).fontSize(13).fontColor('#7F8D70').id('status').width('100%');
}.padding(26).height('100%').width('100%').backgroundColor('#F3F1DF').borderRadius(24);
}
build() {
Row({ space: 18 }) {
if (this.viewportWidth >= 780) { Column() { this.information(); }.width('36%'); }
Column() { this.editor(); }.layoutWeight(1);
}.padding(22).width('100%').height('100%').backgroundColor('#E3E7D5')
.onAreaChange((_old: Area, area: Area) => { this.viewportWidth = Number(area.width); });
}
}
common/WindowTools.ets:
typescript
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
export async function resizeWindow(context: common.UIAbilityContext, widthVp: number,
density: number): Promise<string> {
try {
const mainWindow = await window.getLastWindow(context);
const rect = mainWindow.getWindowProperties().windowRect;
await mainWindow.resize(Math.round(widthVp * density), rect.height);
return '窗口已调整';
} catch (error) {
return '请先将窗口还原为自由窗口,再调整宽度';
}
}

4. 进程重启后的手记

默认手记是"今天也有值得记住的小事"。演示在这段文字后追加"今天完成了一次窗口适配练习",保存并重启后,追加的内容仍在。读取结果可以同最初的默认文字区分,便于检查本地持久化是否真正完成。
重启恢复比单纯点一次保存更有说明力。按钮文字改变,只能说明页面状态更新了;进程结束后还能读到刚写入的内容,才覆盖到这条手记的本地持久化路径。演示中的文字来自实际编辑,没有在应用重启时重新硬编码进默认值。

| 检查项 | 本例观察结果 |
|---|---|
| 模块支持范围 | phone、2in1 |
| 编译环境 | HarmonyOS 6.1.1,API 24 |
| 本次实际运行 | 二合一模拟器 |
| 宽窄布局 | 辅助信息区随宽度显示或收起 |
| 本地数据 | 编辑保存后,重启能够读取 |
动图里的紧凑界面仍然属于二合一自由窗口,展示同一应用在较小区域中的排版。手机端的支持范围由模块配置声明,安装与输入体验可以在相应手机模拟器中继续核对。页面依据可用区域调整布局,数据读写则沿用同一份业务实现。
这几项结果不宜互相代替。窄窗口里能编辑,说明当前布局可以承载输入;重启后还能读到追加文字,说明本地保存路径走通;模块声明两类设备,则说明工程写明了支持范围。把每一项对应到具体证据,后续换设备或更换存储方案时,才知道要重新验证哪一部分。
5. 总结
写下的话能保存,重新打开仍能读到,窗口收窄后也能接着编辑------这就是本例实际跑通的范围。部署声明、可用空间和本地数据分别有自己的依据。以后换成收藏或设置页,也可以挑一条真实操作沿着这三个层次走完,而不只停在欢迎页面上。