@buaa_smat/hometrans 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,13 +13,110 @@ npm install -g @buaa_smat/hometrans
13
13
  ht init
14
14
  ```
15
15
 
16
- One-shot via `npx` (no global install required):
16
+ Requires Node.js **>= 18**.
17
+
18
+ ---
19
+
20
+ ## Setup Guide
21
+
22
+ After running `ht init`, follow these steps to complete the installation:
23
+
24
+ ### Step 1: Choose your target editor
25
+
26
+ `ht init` automatically detects supported editors (Claude Code, Cursor, OpenCode, Codex) and presents an interactive selection prompt. Use **Space** to toggle and **Enter** to confirm.
27
+
28
+ ![Choose target editor](resource/choose_editor.png)
29
+
30
+ ### Step 2: Finish initialization
31
+
32
+ After confirming your editor selection, HomeTrans installs all bundled **skills**, **agents**, and the **MCP server** into each selected editor's configuration directory. The installation is idempotent — re-running `ht init` overwrites only bundled content.
33
+
34
+ ![Finish initialization](resource/finish_init.png)
35
+
36
+ > **Tip:** Run `ht init --all` to skip the editor prompt and install to every detected editor automatically.
37
+
38
+ ---
39
+
40
+ ## HarmonyOS Migration Guide
41
+
42
+ 鸿蒙软件迁移指导,提供完整的 Android 到 HarmonyOS 迁移流程:
43
+
44
+ ### Step 1: Resource Conversion (资源转换)
45
+
46
+ > `hmos-batch-ui-align` 内部会自动调用资源转换;如果是批量UI迁移场景则可跳过此步骤。
47
+
48
+ 将 Android 资源文件(图片、字符串、颜色、尺寸等)转换为 HarmonyOS 格式。
17
49
 
18
- ```bash
19
- npx hometrans init
50
+ ```
51
+ /hmos-resources-convert android-project-path=<安卓项目路径> harmonyos-project-path=<鸿蒙项目路径> resource_mapping_path=<资源映射文档路径>
20
52
  ```
21
53
 
22
- Requires Node.js **>= 18**.
54
+ | 参数 | 类型 | 说明 |
55
+ |------|------|------|
56
+ | `android-project-path` | **必选** | Android 项目根目录路径(含 `build.gradle` 或 `build.gradle.kts`) |
57
+ | `harmonyos-project-path` | **必选** | HarmonyOS 项目输出路径 |
58
+ | `resource_mapping_path` | **必选** | Android ↔ HarmonyOS 资源映射文档的完整输出路径(`.md`) |
59
+ | `apk-path` | 可选 | Android APK 文件路径;提供后跳过 Gradle 构建和 apktool 解包步骤,直接从 APK 提取资源 |
60
+
61
+ ### Step 2: UI Migration (UI迁移)
62
+
63
+ 运行以下命令之一完成 Android UI 的全量或增量迁移。
64
+
65
+ #### 全量迁移 — `hmos-batch-ui-align`
66
+
67
+ ```
68
+ /hmos-batch-ui-align android_project_dir=<安卓项目路径> harmony_project_dir=<鸿蒙项目路径> ui_info_root=<页面快照目录>
69
+ ```
70
+
71
+ | 参数 | 类型 | 说明 |
72
+ |------|------|------|
73
+ | `android_project_dir` | **必选** | Android 项目根目录路径 |
74
+ | `harmony_project_dir` | **必选** | HarmonyOS 项目输出路径 |
75
+ | `ui_info_root` | 可选 | 包含 `page_NNNN_ActivityName` 格式子目录的父目录路径(每子目录含 `meta.json` + `view.xml` + 可选 `screenshot.png`);不提供时 skill 自动通过 ADB 抓取 |
76
+ | `pages` | 可选 | 显式列出的页面子集(如不提供则处理所有页面) |
77
+
78
+ #### 增量迁移 — `hmos-incremental-ui-align`
79
+
80
+ 项目路径通过 `config.json` 文件配置(参考 skill 目录下的 `config-example.json`),对齐目标在消息中描述(如 "帮我对齐设置页面的关于页面")。
81
+
82
+ ```
83
+ /hmos-incremental-ui-align config-path=<config.json路径>
84
+ ```
85
+
86
+ | 参数 | 类型 | 说明 |
87
+ |------|------|------|
88
+ | `config-path` | **必选** | `config.json` 文件路径,包含 `android.project_dir`, `harmony.project_dir`, `hmos_sdk_dir`, `glm_api_key`, `capture_output_dir` 等字段 |
89
+
90
+ ### Step 3: Generate Spec (生成需求规格)
91
+
92
+ 运行 `hmos-spec-generate` 从原始需求描述 `.txt` 文件生成原子场景需求规格文档。
93
+
94
+ ```
95
+ /hmos-spec-generate requirement-description-file=<需求描述文件路径> android-project-path=<安卓项目路径> spec-output-dir=<规格输出目录>
96
+ ```
97
+
98
+ | 参数 | 类型 | 说明 |
99
+ |------|------|------|
100
+ | `requirement-description-file` | **必选** | 需求描述 `.txt` 文件路径(每段以 `REQ` 开头,空行分隔) |
101
+ | `android-project-path` | **必选** | Android 项目根目录路径(必须位于 Git 仓库内) |
102
+ | `spec-output-dir` | **必选** | 规格文档输出目录(自动创建;每个 REQ 生成 `<feature>-SPEC.md` + `.trace/<feature>.md`) |
103
+
104
+ ### Step 4: Convert Pipeline (逻辑代码开发)
105
+
106
+ 运行 `hmos-convert-pipeline` 完成后续的逻辑代码开发、代码检视、集成测试流程。
107
+
108
+ ```
109
+ /hmos-convert-pipeline android-project-path=<安卓项目路径> harmonyos-project-path=<鸿蒙项目路径> assets-output-path=<输出报告目录>
110
+ ```
111
+
112
+ | 参数 | 类型 | 说明 |
113
+ |------|------|------|
114
+ | `android-project-path` | **必选** | Android 项目根目录路径 |
115
+ | `harmonyos-project-path` | **必选** | HarmonyOS 项目根目录路径 |
116
+ | `assets-output-path` | **必选** | 输出/报告文件的存放目录(需包含 `plan.md` 需求规格文件) |
117
+ | `max-rounds-review` | 可选 | 代码检视循环最大轮数(正整数 `>= 1`,默认 `2`) |
118
+ | `max-rounds-test` | 可选 | 自测循环最大轮数(正整数 `>= 1`,默认 `2`) |
119
+ | `skip-test` | 可选 | `true` 跳过集成测试阶段(无真机验证环境时需要设置为true,默认 `false`) |
23
120
 
24
121
  ---
25
122
 
@@ -66,55 +163,17 @@ Requires Node.js **>= 18**.
66
163
 
67
164
  ---
68
165
 
69
- ## What `ht mcp` does
70
-
71
- Starts a stdio-mode MCP server exposing the following tool:
72
-
73
- ### `extract_commit_context`
74
-
75
- Extracts code context index from a given git commit in a HarmonyOS project (git diff + ArkTS semantic dependency analysis), for use during AI-assisted Android-to-HarmonyOS code review.
76
-
77
- **Input parameters:**
78
-
79
- | Parameter | Type | Required | Description |
80
- |-----------|------|----------|-------------|
81
- | `projectPath` | string | Yes | Absolute path to HarmonyOS project root (must contain `.git`) |
82
- | `commitId` | string | Yes | Git commit ID (diffs against its first parent) |
83
- | `mode` | string | No | `"default"` builds full call graph; other values skip (default: `"default"`) |
84
- | `ohosSdkPath` | string | No | OpenHarmony SDK ETS path; falls back to env var `OHOS_SDK_PATH` |
85
- | `hmsSdkPath` | string | No | HMS SDK ETS path; falls back to env var `HMS_SDK_PATH` |
86
-
87
- **Output:** `{path, kind, ranges?, resourceNames?}[]`
88
- - `source` files: 1-based `[start, end]` line-range arrays
89
- - `resource` files: list of referenced resource names
90
-
91
- ---
92
-
93
166
  ## Skills
94
167
 
95
- | Skill | Trigger phrases | Description |
96
- |-------|-----------------|-------------|
97
- | `hmos-convert-pipeline` | "full Android-to-HarmonyOS pipeline", "run the conversion pipeline end-to-end", "hmos-convert-pipeline" | Runs all conversion agents in sequence with progress tracking, duration stats, and defect recording. 9 stages: Logic Context → Logic Coding → Build → Code Review → Review Fix Rebuild Self-Test Self-Test Fix Rebuild |
98
- | `hmos-spec-generate` | "spec generation", "generate spec", "requirement to spec", "atomic scenarios", "scenario decomposition", "req to spec" | Generates atomic-scenario requirement specs from raw `.txt` requirement batches. Reads REQ blocks, explores the Android code graph via GitNexus, writes per-REQ trace files, then synthesizes specs from the trace |
99
- | `hmos-resources-convert` | "Android resources to HarmonyOS", "migrate Android res", "convert drawables/strings/colors" | Converts Android project resources (strings, colors, dimensions, images, drawables, icons) into HarmonyOS resources, including qualifier directories and XML→JSON format conversion |
100
- | `hmos-incremental-ui-align` | "UI对齐", "页面对齐", "和安卓对齐", "鸿蒙页面修复", "UI增量开发", "align HarmonyOS with Android" | Automated HarmonyOS-Android UI alignment: navigates to target pages on both devices, captures view trees + screenshots, then aligns HarmonyOS code to match Android |
101
- | `hmos-batch-ui-align` | "把安卓页面迁移到鸿蒙", "Android UI 转鸿蒙", "批量转 ArkTS" | Batch-converts multiple Android Activity UI snapshots (`page_NNNN_ActivityName`) to HarmonyOS ArkUI (ArkTS) pages |
102
- | `hmos-integration-test` | "跑自测", "运行自测", "自动测试", "设备测试", "self test", "run autotest" | Runs on-device self-test for a HarmonyOS app: parses `test_case.md`, installs the HAP, executes AutoTest, and produces a verification report — optionally entering a test-and-fix loop |
103
-
104
- ### `hmos-convert-pipeline` Arguments
105
-
106
- 1. `android-project-path` (required): path to the Android source project
107
- 2. `harmonyos-project-path` (required): path to the target HarmonyOS project
108
- 3. `assets-output-path` (required): directory for output/report files
109
- 4. `max-rounds-review` (optional, default `2`): max Code Review → Fix loop rounds
110
- 5. `max-rounds-test` (optional, default `2`): max Self-Test → Fix loop rounds
111
- 6. `variant` (optional, default `enhanced`): `enhanced` | `baseline` — selects the Stage 1/1a agent family
112
-
113
- ### `hmos-spec-generate` Arguments
114
-
115
- 1. `requirement-description-file` (required): absolute path to a single `.txt` file containing one or more REQ blocks separated by blank lines
116
- 2. `android-project-path` (required): absolute path to the Android project root (must be inside a Git repo)
117
- 3. `spec-output-dir` (required): absolute path to the directory where spec files will be written
168
+ | Skill | Trigger phrases | Description | Prerequisites | Arguments | Example |
169
+ |-------|-----------------|-------------|---------------|-----------|--------|
170
+ | `hmos-convert-pipeline` | "full Android-to-HarmonyOS pipeline", "run the conversion pipeline end-to-end", "hmos-convert-pipeline" | Runs all conversion agents in sequence with progress tracking, duration stats, and defect recording. 4 stages: Logic Development (Context Builder) → Logic Coding → Build → Code Review/Fix/Rebuild loop Self-Test/Fix/Rebuild loop | `assets-output-path` 下需存在 `plan.md` 需求规格文件 | `android-project-path` (required), `harmonyos-project-path` (required), `assets-output-path` (required), `max-rounds-review` (optional, default 2), `max-rounds-test` (optional, default 2), `skip-test` (optional, default false) | `"/hmos-convert-pipeline android-project-path=D:/path/to/android harmonyos-project-path=D:/path/to/harmonyos assets-output-path=D:/path/to/output"` |
171
+ | `hmos-spec-generate` | "spec generation", "generate spec", "requirement to spec", "atomic scenarios", "scenario decomposition", "req to spec" | Generates atomic-scenario requirement specs from raw `.txt` requirement batches. Reads REQ blocks, explores the Android code graph via GitNexus, writes per-REQ trace files, then synthesizes specs from the trace | 需求描述文件 (`.txt`),每段以 `REQ` 开头 | `requirement-description-file` (required), `android-project-path` (required), `spec-output-dir` (required) | `"/hmos-spec-generate requirement-description-file=D:/path/to/req.txt android-project-path=D:/path/to/android spec-output-dir=D:/path/to/specs"` |
172
+ | `hmos-resources-convert` | "Android resources to HarmonyOS", "migrate Android res", "convert drawables/strings/colors" | Converts Android project resources (strings, colors, dimensions, images, drawables, icons) into HarmonyOS resources, including qualifier directories and XML→JSON format conversion | Android 项目中需包含 `res/` 资源目录 | `android-project-path` (required), `harmonyos-project-path` (required), `resource_mapping_path` (required), `apk-path` (optional) | `"/hmos-resources-convert android-project-path=D:/path/to/android harmonyos-project-path=D:/path/to/harmonyos resource_mapping_path=D:/path/to/resource_mapping.md"` |
173
+ | `hmos-incremental-ui-align` | "UI对齐", "页面对齐", "和安卓对齐", "鸿蒙页面修复", "UI增量开发", "align HarmonyOS with Android" | Automated HarmonyOS-Android UI alignment: navigates to target pages on both devices, captures view trees + screenshots, then aligns HarmonyOS code to match Android | 需连接 Android 设备进行 UI 对比;`config-path` 指向的 `config.json` 中需配置项目路径、SDK 路径、API key | `config-path` (required) | `"/hmos-incremental-ui-align config-path=D:/path/to/config.json 帮我对齐设置页面的关于页面"` |
174
+ | `hmos-batch-ui-align` | "把安卓页面迁移到鸿蒙", "Android UI 转鸿蒙", "批量转 ArkTS" | Batch-converts multiple Android Activity UI snapshots (`page_NNNN_ActivityName`) to HarmonyOS ArkUI (ArkTS) pages | `ui_info_root` 下需包含 `page_NNNN_ActivityName` 格式的页面快照子目录 | `android_project_dir` (required), `harmony_project_dir` (required), `ui_info_root` (optional), `pages` (optional) | `"/hmos-batch-ui-align android_project_dir=D:/path/to/android harmony_project_dir=D:/path/to/harmonyos ui_info_root=D:/path/to/pages"` |
175
+ | `hmos-integration-test` | "跑自测", "运行自测", "自动测试", "设备测试", "self test", "run autotest" | Runs on-device self-test for a HarmonyOS app: parses `test_case.md`, installs the HAP, executes AutoTest, and produces a verification report — optionally entering a test-and-fix loop | 需存在 `test_case.md` 测试用例文件,设备需安装待测 HAP | `test-case-path` (required), `hap-path` (required), `output-path` (optional), `pre-test-case-path` (optional), `android-project-path` (optional), `max-rounds` (optional, default 3) | `"/hmos-integration-test test-case-path=D:/path/to/test_case.md hap-path=D:/path/to/app-signed.hap"` |
176
+ | `hmos-fix-build-errors` | "build HarmonyOS project", "fix compile errors", "auto build and fix", "hmos-fix-build-errors" | Builds a HarmonyOS NEXT project from the command line, parses compile errors, fixes them, and retries in a loop until the build succeeds. Default produces an unsigned HAP; `--signed` produces a signed HAP | 有效的 HarmonyOS 项目(含 `build-profile.json5`、`entry/src`、`oh-package.json5`)+ DevEco Studio 安装目录;`--signed` 时签名配置须已存在于 `build-profile.json5` | `harmonyos-project-path` (required), `deveco-studio-path` (required), `--signed` (optional) | `"/hmos-fix-build-errors D:/MyHmosApp \"D:/DevEco Studio\" --signed"` |
118
177
 
119
178
  ---
120
179
 
@@ -125,14 +184,15 @@ Extracts code context index from a given git commit in a HarmonyOS project (git
125
184
  | `logic-context-builder` | Constrains HarmonyOS ArkTS app changes into an executable decision contract |
126
185
  | `logic-coder` | Executes HarmonyOS ArkTS code implementation from the decision contract (ships with `scripts/platform_context_query.py`) |
127
186
  | `build-fixer` | Automatically builds a HarmonyOS project, parses compile errors, fixes them, and retries in a loop until the build succeeds |
128
- | `code-reviewer` | Reviews HarmonyOS code against user scenarios to validate functional coverage || `review-fixer` | Fixes issues from code review reports — verifies each issue before fixing, references Android source, uses cautious per-scenario fix strategies |
187
+ | `code-reviewer` | Reviews HarmonyOS code against user scenarios to validate functional coverage |
188
+ | `review-fixer` | Fixes issues from code review reports — verifies each issue before fixing, references Android source, uses cautious per-scenario fix strategies |
129
189
  | `spec-generator` | Generates requirement spec documents for each requirement description file in a folder by exploring the Android codebase and decomposing into atomic user scenarios |
130
190
  | `self-tester` | Unified self-test agent — parses `test_case.md` into `testcases.json`, runs on-device AutoTest verification, and produces a report. A `setup` boolean controls whether the parse phase runs (skip on round-2+ to reuse prior artifacts) |
131
191
  | `self-test-fixer` | Fixes issues identified by self-testing — reads the self-test report, white-box verifies failures, plans and executes fixes in order |
132
192
 
133
193
  ---
134
194
 
135
- MIT
195
+
136
196
 
137
197
  <p align="center">
138
198
  <a href="https://gitcode.com/SMAT/HomeTrans">Official Repository</a> •
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@buaa_smat/hometrans",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "HomeTrans (Android-to-HarmonyOS) skill + agent installer. Run `ht init` to distribute conversion skills and subagents into AI editors.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://gitcode.com/SMAT/HomeTrans"
9
+ },
6
10
  "type": "module",
7
11
  "bin": {
8
12
  "hometrans": "dist/cli/index.js",
@@ -12,7 +16,9 @@
12
16
  "dist",
13
17
  "skills",
14
18
  "agents",
15
- "tools"
19
+
20
+ "tools",
21
+ "resource"
16
22
  ],
17
23
  "scripts": {
18
24
  "build": "node scripts/build.js",
Binary file
Binary file
@@ -20,10 +20,9 @@ Parse `$ARGUMENTS` as positional tokens:
20
20
  - **Arg 3** (`assets-output-path`): Directory to store all output/report files (required)
21
21
  - **Arg 4** (`max-rounds-review`): Maximum number of Stage 3→3a→3b code-review-fix rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
22
22
  - **Arg 5** (`max-rounds-test`): Maximum number of Stage 4→4a→4b self-test rounds to run (optional, default `2`). Must be a positive integer `>= 1`.
23
- - **Arg 6** (`variant`): `enhanced` or `baseline` (optional, default `enhanced`). Selects the Stage 1 / 1a agent family. `enhanced` uses the full `logic-context-builder` + `logic-coder` agents; `baseline` uses the pure-LLM `logic-context-builder-minimal` + `logic-coding-minimal` agents for A/B capability comparison. All other stages and their agents are unchanged. To pass this arg, provide explicit values for Args 4 and 5 first.
24
- - **Arg 7** (`skip-test`): `true` or `false` (optional, default `false`). When `true`, skip Stage 4 / 4a / 4b (Self-Testing Loop) entirely. Use this when no real HarmonyOS device is available for on-device testing. To pass this arg, provide explicit values for Args 4, 5, and 6 first.
23
+ - **Arg 6** (`skip-test`): `true` or `false` (optional, default `false`). When `true`, skip Stage 4 / 4a / 4b (Self-Testing Loop) entirely. Use this when no real HarmonyOS device is available for on-device testing. To pass this arg, provide explicit values for Args 4 and 5 first.
25
24
 
26
- If any required argument is missing, ask the user before proceeding. If `max-rounds-review` or `max-rounds-test` is provided but is not a positive integer, ask the user before proceeding. If `variant` is provided but is not `enhanced` or `baseline`, ask the user before proceeding. If `skip-test` is provided but is not `true` or `false`, ask the user before proceeding.
25
+ If any required argument is missing, ask the user before proceeding. If `max-rounds-review` or `max-rounds-test` is provided but is not a positive integer, ask the user before proceeding. If `skip-test` is provided but is not `true` or `false`, ask the user before proceeding.
27
26
 
28
27
  Define shorthand variables for the instructions below:
29
28
 
@@ -34,7 +33,6 @@ Define shorthand variables for the instructions below:
34
33
  | `OUTPUT` | assets-output-path (resolved or generated) |
35
34
  | `MAX_ROUNDS_REVIEW` | max-rounds-review — positive integer (default `2`). Controls Stage 3→3a→3b code-review-fix loop. |
36
35
  | `MAX_ROUNDS_TEST` | max-rounds-test — positive integer (default `2`). Controls Stage 4→4a→4b self-test loop. |
37
- | `VARIANT` | variant — `enhanced` (default) or `baseline`. `baseline` switches Stage 1/1a to the minimal pure-LLM baseline agents. |
38
36
  | `SKIP_TEST` | skip-test — `true` or `false` (default `false`). When `true`, skip Stage 4 / 4a / 4b entirely (no real device available). |
39
37
 
40
38
  ---
@@ -145,40 +143,26 @@ Flags: `skip-plan-builder: true|false` (default `false`) — when `true`, skip t
145
143
 
146
144
  Prompt format (applies to both Stage 1 and Stage 1a): ONLY the key-value lines below. No natural language, step lists, or Markdown.
147
145
 
148
- 1. Launch the logic context builder agent. Pick by `VARIANT`:
146
+ 1. Launch the logic context builder agent:
149
147
  ```
150
- # VARIANT=enhanced (default) — full agent
151
148
  Agent(
152
149
  subagent_type="logic-context-builder",
153
150
  prompt="harmonyos-project-path: HMOS\nspec-file: OUTPUT/plan.md\noutput-path: OUTPUT/logic"
154
151
  )
155
-
156
- # VARIANT=baseline — pure-LLM baseline agent
157
- Agent(
158
- subagent_type="logic-context-builder-minimal",
159
- prompt="harmonyos-project-path: HMOS\nspec-file: OUTPUT/plan.md\noutput-path: OUTPUT/logic"
160
- )
161
152
  ```
162
153
  2. Verify `OUTPUT/logic/plan.md` exists.
163
154
 
164
155
  ### Stage 1a — Logic Coding
165
156
 
166
- 1. Launch the logic coding agent. Pick by `VARIANT`:
157
+ 1. Launch the logic coding agent:
167
158
  ```
168
- # VARIANT=enhanced (default) — full agent
169
159
  Agent(
170
160
  subagent_type="logic-coder",
171
161
  prompt="harmonyos-project-path: HMOS\nplan-file: OUTPUT/logic/plan.md\noutput-path: OUTPUT/logic"
172
162
  )
173
-
174
- # VARIANT=baseline — pure-LLM baseline agent
175
- Agent(
176
- subagent_type="logic-coding-minimal",
177
- prompt="harmonyos-project-path: HMOS\nplan-file: OUTPUT/logic/plan.md\noutput-path: OUTPUT/logic"
178
- )
179
163
  ```
180
164
  2. Copy `OUTPUT/logic/commit-info.md` → `OUTPUT/commit-info.md` and verify it contains a hex `commit-id`.
181
- 3. `OUTPUT/logic/coding-summary.md` (enhanced mode only) is human-readable auxiliary; downstream stages consume only `OUTPUT/commit-info.md`.
165
+ 3. `OUTPUT/logic/coding-summary.md` is human-readable auxiliary; downstream stages consume only `OUTPUT/commit-info.md`.
182
166
 
183
167
  ### Stage 2 — Compilation and Build
184
168
 
@@ -17,23 +17,25 @@ You are writing ArkTS codes.
17
17
  - 不要做和用户需求无关的其他修改
18
18
  -
19
19
  ## Step 0: Load Config
20
- The user MUST specify the path to a `config.json` file when invoking this skill (e.g., as part of the request: "config: D:\path\to\config.json"). Read that file from the user-provided path. If the user did not provide a path, ask them for it before proceeding. See `config-example.json` in this skill's directory for the expected schema.
21
-
22
- The config provides all fixed paths so the user only describes what needs to be aligned.
23
- Key config fields:
24
- - `android.app_name`, `android.package` Android app info for navigation
25
- - `android.project_dir` — Android source code root
26
- - `harmony.app_name`, `harmony.package` HarmonyOS app info for navigation
27
- - `harmony.project_dir` HarmonyOS project root
28
- - `hmos_sdk_dir` HarmonyOS SDK path for API reference
29
- - `glm_api_key` Zhipu GLM API key for phone-agent
30
- - `capture_output_dir` base directory for captured page data
31
- - `navigation_tool` = `scripts/app_feature_verify.py`
32
- - `capture_tool` = `scripts/page_capture.py`
20
+ The user MUST provide a `config-path` when invoking this skill (e.g., `config-path: D:\path\to\config.json`). The config file must exist before running verify with a Read, and if it's missing or unreadable, ask the user for a valid path. Refer to `config-example.json` in this skill's directory for the expected schema.
21
+
22
+ Config fields:
23
+
24
+ | Field | Description |
25
+ |---|---|
26
+ | `android.app_name` | Android app name for navigation |
27
+ | `android.package` | Android app package name |
28
+ | `android.project_dir` | Android source code root path |
29
+ | `harmony.app_name` | HarmonyOS app name for navigation |
30
+ | `harmony.package` | HarmonyOS app package name |
31
+ | `harmony.project_dir` | HarmonyOS project root path |
32
+ | `hmos_sdk_dir` | HarmonyOS SDK path (ETS API reference) |
33
+ | `glm_api_key` | Zhipu GLM API key for phone-agent |
34
+ | `capture_output_dir` | Base directory for captured page data |
33
35
 
34
36
  ## Step 1: Capture All Related Pages on Android & HarmonyOS Devices
35
37
 
36
- Read `scripts/navigation-capure.md` to learn the usage of `navigation_tool` and `capture_tool`.
38
+ Read `scripts/navigation-capure.md` to learn the usage of `scripts/app_feature_verify.py` (navigation) and `scripts/page_capture.py` (capture).
37
39
 
38
40
  ### Step 1.1: Parse User Request and Build Capture Plan
39
41
  Analyze the user's description and build a list of **base pages** to capture.
@@ -48,8 +50,8 @@ Create the timestamped output directory and per-page sub-directories following t
48
50
 
49
51
  ### Step 1.2: Capture Base Pages
50
52
  For each base page in the plan, on **both** Android and HarmonyOS devices:
51
- 1. Use `navigation_tool` to navigate to the page.
52
- 2. On success, use `capture_tool` to capture the view tree and screenshot.
53
+ 1. Use `scripts/app_feature_verify.py` to navigate to the page.
54
+ 2. On success, use `scripts/page_capture.py` to capture the view tree and screenshot.
53
55
  3. For HarmonyOS pages that don't exist yet, navigation will fail — leave the directory empty (expected).
54
56
 
55
57
  ### Step 1.3: Discover and Capture Interactive States
@@ -61,7 +63,7 @@ After capturing each base page, scan its view tree for **interactive elements th
61
63
 
62
64
  **For each interactive element found:**
63
65
  1. Create a separate capture directory: `{platform}_page_{i}_{base_name}_{state_type}_{state_name}` (e.g., `android_page_1_detail_tab_city`, `android_page_1_detail_popup_filter_identity`).
64
- 2. Navigate to the base page (reuse the same nav path), then append the click action to trigger the state change. Capture with `capture_tool`.
66
+ 2. Navigate to the base page (reuse the same nav path), then append the click action to trigger the state change. Capture with `scripts/page_capture.py`.
65
67
  3. Repeat for **both** Android and HarmonyOS devices.
66
68
  4. Each captured state is treated as a separate page pair in Step 2 and Step 3.
67
69
 
@@ -1,911 +0,0 @@
1
- # MVVM模式(V1)
2
-
3
- 当开发者掌握了状态管理的基本概念后,通常会尝试开发自己的应用,在应用开发初期,如果未能精心规划项目结构,随着项目扩展和复杂化,状态变量的增多将导致组件间关系变得错综复杂。此时,开发新功能可能引起连锁反应,维护成本也会增加。为此,本文旨在介绍MVVM模式以及ArkUI的UI开发模式与MVVM的关系,指导开发者如何设计项目结构,以便在产品迭代和升级时能更轻松地开发和维护。
4
-
5
- 本文档涵盖了大多数状态管理V1装饰器,所以在阅读本文档前,建议开发者对状态管理V1有一定的了解。建议提前阅读:[状态管理概述](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state-management-overview)和状态管理V1装饰器相关文档。
6
-
7
- ## MVVM模式介绍
8
-
9
- ### 概念
10
-
11
- 在应用开发中,UI更新需要实时同步数据状态变化,这直接影响应用程序的性能和用户体验。为了解决数据与UI同步的复杂性,ArkUI采用了 Model-View-ViewModel(MVVM)架构模式。MVVM 将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。通过这种模式,UI可以自动更新状态变化,从而更高效地管理数据和视图的绑定与更新。
12
-
13
- - **View**:用户界面层。负责用户界面展示并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据实现动态更新。
14
- - **Model**:数据访问层。以数据为中心,不直接与用户界面交互。负责数据结构定义,数据管理(获取、存储、更新等),以及业务逻辑处理。
15
- - **ViewModel**:表示逻辑层。作为连接Model和View的桥梁,通常一个View对应一个ViewModel。View和ViewModel有两种通信方式:
16
- 1. 方法调用:View通过事件监听用户行为,在回调里面触发ViewModel层的方法。例如当View监听到用户Button点击行为,调用ViewModel对应的方法,处理用户操作。
17
- 2. 双向绑定:View绑定ViewModel的数据,实现双向同步。
18
-
19
- ArkUI的UI开发模式就属于MVVM模式,通过对MVVM概念的基本介绍,开发者大致能猜到状态管理能在MVVM中起什么样的作用,状态管理旨在数据驱动更新,让开发者只用关注页面设计,而不去关注整个UI的刷新逻辑,数据的维护也无需开发者进行感知,由状态变量自动更新完成,而这就是属于ViewModel层所需要支持的内容,因此开发者使用MVVM模式开发自己的应用是最省心省力的。
20
-
21
- ### ArkUI开发模式图
22
-
23
- ArkUI的UI开发模式即是MVVM模式,而状态变量在MVVM模式中扮演着ViewModel的角色,向上刷新UI,向下更新数据
24
-
25
- ### 分层说明
26
-
27
- **View层**
28
-
29
- View层通常可以分为下列组件:
30
-
31
- - **页面组件**:所有应用基本都是按照页面进行分类的,比如登录页,列表页,编辑页,帮助页,版权页等。每个页面对应需要的数据可能是完全不一样的,也可能多个页面需要的数据是同一套。
32
- - **业务组件**:本身具备本APP部分业务能力的功能组件,典型的就是这个业务组件可能关联了本项目的ViewModel中的数据,不可以被共享给其他项目使用。
33
- - **通用组件**:像系统组件一样,这类组件不会关联本APP中ViewModel的数据,这些组件可实现跨越多个项目进行共享,来完成比较通用的功能。
34
-
35
- **Model层**
36
-
37
- Model层是应用的原始数据提供者,代表应用的核心业务逻辑和数据。
38
-
39
- **ViewModel层**
40
-
41
- 为View层的组件提供对应数据,按照页面组织,当用户浏览页面时,某些页面可能不会被显示,因此,页面数据最好设计成懒加载(按需加载)的模式。
42
-
43
- ViewModel层数据和Model层数据的区别:
44
-
45
- - Model层数据是按照整个工程、项目来组织数据,构成一套完整的APP业务数据体系。
46
- - ViewModel层数据,是提供某个页面上使用的数据,它可能是整个APP的业务数据的一部分。另外ViewModel层还可以附加对应Page的辅助页面显示数据,这部分数据可能与本APP的业务完全无关,仅仅是为页面展示提供便利的辅助数据。
47
-
48
- ### 架构核心原则
49
-
50
- **不可跨层访问**
51
-
52
- - View层不可以直接调用Model层的数据,只能通过ViewModel提供的方法进行调用。
53
- - Model层不能直接操作UI,只能通知ViewModel层数据有更新,由ViewModel层更新对应的数据。
54
-
55
- **下层不可访问上层数据**
56
-
57
- 下层数据通过通知模式更新上层数据。在业务逻辑中,下层不可直接获取上层数据。例如,ViewModel层的逻辑处理不应该依赖View层界面上的某个值。
58
-
59
- **非父子组件间不可直接访问**
60
-
61
- 这是针对View层设计的核心原则,一个组件应该具备以下逻辑:
62
-
63
- - 禁止直接访问父组件(必须使用事件或是订阅能力)。
64
- - 禁止直接访问兄弟组件。这是因为组件应该仅能访问自己的子节点(通过传参)和父节点(通过事件或通知),以此完成组件之间的解耦。
65
-
66
- 对于一个组件,这样设计的原因如下:
67
-
68
- - 组件自己使用了哪些子组件是明确的,因此可以访问。
69
- - 组件被放置于哪个父节点下是未知的,因此组件想访问父节点,就只能通过通知或者事件能力完成。
70
- - 组件不可能知道自己的兄弟节点是谁,因此组件不可以操作兄弟节点。
71
-
72
- ## 备忘录开发实战
73
-
74
- 本节通过备忘录应用的开发,使开发者了解如何使用ArkUI框架设计自己的应用。本节直接进行功能开发,未设计代码架构,即根据需求即时开发,不考虑后续维护,同时,本节还将介绍功能开发所需的装饰器。
75
-
76
- ### @State状态变量
77
-
78
- [@State](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state)装饰器是最常用的装饰器之一,用于定义状态变量。通常,这些状态变量作为父组件的数据源,开发者点击时,触发状态变量的更新,刷新UI。
79
-
80
- ```typescript
81
- @Entry
82
- @Component
83
- struct StateIndex {
84
- @State isFinished: boolean = false;
85
-
86
- build() {
87
- Column() {
88
- Row() {
89
- // 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
90
- Text($r('app.string.all_tasks'))
91
- .fontSize(30)
92
- .fontWeight(FontWeight.Bold)
93
- }
94
- .width('100%')
95
- .margin({ top: 10, bottom: 10 })
96
-
97
- // 待办事项
98
- Row({ space: 15 }) {
99
- if (this.isFinished) {
100
- // 请将$r('app.media.finished')替换为实际资源文件
101
- Image($r('app.media.finished'))
102
- .width(28)
103
- .height(28)
104
- } else {
105
- // 请将$r('app.media.unfinished')替换为实际资源文件
106
- Image($r('app.media.unfinished'))
107
- .width(28)
108
- .height(28)
109
- }
110
- // 请将$r('app.string.all_learn_advanced_math')替换为实际资源文件,在本示例中该资源文件的value值为"学习高数"
111
- Text($r('app.string.learn_advanced_math'))
112
- .fontSize(24)
113
- .decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
114
- }
115
- .height('40%')
116
- .width('100%')
117
- .border({ width: 5 })
118
- .padding({ left: 15 })
119
- .onClick(() => {
120
- this.isFinished = !this.isFinished;
121
- })
122
- }
123
- .height('100%')
124
- .width('100%')
125
- .margin({ top: 5, bottom: 5 })
126
- .backgroundColor('#90f1f3f5')
127
- }
128
- }
129
- ```
130
-
131
- ### @Prop、@Link的作用
132
-
133
- 上述示例中,所有代码都写在了@Entry组件中。随着需要渲染的组件越来越多,@Entry组件必然需要进行拆分,为此,拆分出的子组件就需要使用@Prop和@Link装饰器:
134
-
135
- - [@Prop](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-prop)是父子间单向传递,子组件会深拷贝父组件数据,可从父组件更新,也可自己更新数据,但不会同步回父组件。
136
- - [@Link](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-link)是父子间双向传递,父组件改变,会通知所有的@Link,同时@Link的更新也会通知父组件的数据源进行刷新。
137
-
138
- ```typescript
139
- @Component
140
- struct PropLinkTodoComponent {
141
- build() {
142
- Row() {
143
- // 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
144
- Text($r('app.string.all_tasks'))
145
- .fontSize(30)
146
- .fontWeight(FontWeight.Bold)
147
- }
148
- .width('100%')
149
- .margin({ top: 10, bottom: 10 })
150
- }
151
- }
152
-
153
- @Component
154
- struct PropLinkAllChooseComponent {
155
- @Link isFinished: boolean;
156
-
157
- build() {
158
- Row() {
159
- // 请将$r('app.string.check_all')替换为实际资源文件,在本示例中该资源文件的value值为"全选"
160
- Button($r('app.string.check_all'), { type: ButtonType.Normal })
161
- .onClick(() => {
162
- this.isFinished = !this.isFinished;
163
- })
164
- .fontSize(30)
165
- .fontWeight(FontWeight.Bold)
166
- .backgroundColor('#f7f6cc74')
167
- }
168
- .padding({ left: 15 })
169
- .width('100%')
170
- .margin({ top: 10, bottom: 10 })
171
- }
172
- }
173
-
174
- @Component
175
- struct ThingComponent1 {
176
- @Prop isFinished: boolean;
177
-
178
- build() {
179
- // 待办事项1
180
- Row({ space: 15 }) {
181
- if (this.isFinished) {
182
- // 请将$r('app.media.finished')替换为实际资源文件
183
- Image($r('app.media.finished'))
184
- .width(28)
185
- .height(28)
186
- } else {
187
- // 请将$r('app.media.unfinished')替换为实际资源文件
188
- Image($r('app.media.unfinished'))
189
- .width(28)
190
- .height(28)
191
- }
192
- // 请将$r('app.string.learn_chinese')替换为实际资源文件,在本示例中该资源文件的value值为"学习语文"
193
- Text($r('app.string.learn_chinese'))
194
- .fontSize(24)
195
- .decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
196
- }
197
- .height('40%')
198
- .width('100%')
199
- .border({ width: 5 })
200
- .padding({ left: 15 })
201
- .onClick(() => {
202
- this.isFinished = !this.isFinished;
203
- })
204
- }
205
- }
206
-
207
- @Component
208
- struct ThingComponent2 {
209
- @Prop isFinished: boolean;
210
-
211
- build() {
212
- // 待办事项1
213
- Row({ space: 15 }) {
214
- if (this.isFinished) {
215
- // 请将$r('app.media.finished')替换为实际资源文件
216
- Image($r('app.media.finished'))
217
- .width(28)
218
- .height(28)
219
- } else {
220
- // 请将$r('app.media.unfinished')替换为实际资源文件
221
- Image($r('app.media.unfinished'))
222
- .width(28)
223
- .height(28)
224
- }
225
- // 请将$r('app.string.learn_advanced_math')替换为实际资源文件,在本示例中该资源文件的value值为"学习高数"
226
- Text($r('app.string.learn_advanced_math'))
227
- .fontSize(24)
228
- .decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
229
- }
230
- .height('40%')
231
- .width('100%')
232
- .border({ width: 5 })
233
- .padding({ left: 15 })
234
- .onClick(() => {
235
- this.isFinished = !this.isFinished;
236
- })
237
- }
238
- }
239
-
240
- @Entry
241
- @Component
242
- struct PropLinkIndex {
243
- @State isFinished: boolean = false;
244
-
245
- build() {
246
- Column() {
247
- // 全部待办
248
- PropLinkTodoComponent()
249
-
250
- // 全选
251
- PropLinkAllChooseComponent({ isFinished: this.isFinished })
252
-
253
- // 待办事项1
254
- ThingComponent1({ isFinished: this.isFinished })
255
-
256
- // 待办事项2
257
- ThingComponent2({ isFinished: this.isFinished })
258
- }
259
- .height('100%')
260
- .width('100%')
261
- .margin({ top: 5, bottom: 5 })
262
- .backgroundColor('#90f1f3f5')
263
- }
264
- }
265
- ```
266
-
267
- ### 循环渲染组件
268
-
269
- - 上个示例虽然拆分出了子组件,但发现组件1和组件2的代码非常相似,当渲染的组件除了数据外,其他设置都相同时,此时就需要使用[ForEach循环渲染](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-rendering-control-foreach)。
270
- - ForEach使用之后,冗余代码变得更少,并且代码结构更加清晰。
271
-
272
- ```typescript
273
- @Component
274
- struct ForEachTodoComponent {
275
- build() {
276
- Row() {
277
- // 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
278
- Text($r('app.string.all_tasks'))
279
- .fontSize(30)
280
- .fontWeight(FontWeight.Bold)
281
- }
282
- .width('100%')
283
- .margin({ top: 10, bottom: 10 })
284
- }
285
- }
286
-
287
- @Component
288
- struct ForEachAllChooseComponent {
289
- @Link isFinished: boolean;
290
-
291
- build() {
292
- Row() {
293
- // 请将$r('app.string.check_all')替换为实际资源文件,在本示例中该资源文件的value值为"全选"
294
- Button($r('app.string.check_all'), { type: ButtonType.Normal })
295
- .onClick(() => {
296
- this.isFinished = !this.isFinished;
297
- })
298
- .fontSize(30)
299
- .fontWeight(FontWeight.Bold)
300
- .backgroundColor('#f7f6cc74')
301
- }
302
- .padding({ left: 15 })
303
- .width('100%')
304
- .margin({ top: 10, bottom: 10 })
305
- }
306
- }
307
-
308
- @Component
309
- struct ForEachThingComponent {
310
- @Prop isFinished: boolean;
311
- @Prop thing: string;
312
-
313
- build() {
314
- // 待办事项1
315
- Row({ space: 15 }) {
316
- if (this.isFinished) {
317
- // 请将$r('app.media.finished')替换为实际资源文件
318
- Image($r('app.media.finished'))
319
- .width(28)
320
- .height(28)
321
- } else {
322
- // 请将$r('app.media.unfinished')替换为实际资源文件
323
- Image($r('app.media.unfinished'))
324
- .width(28)
325
- .height(28)
326
- // ...
327
- }
328
- Text(`${this.thing}`)
329
- .fontSize(24)
330
- .decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
331
- }
332
- .height('8%')
333
- .width('90%')
334
- .padding({ left: 15 })
335
- .opacity(this.isFinished ? 0.3 : 1)
336
- .border({ width: 1 })
337
- .borderColor(Color.White)
338
- .borderRadius(25)
339
- .backgroundColor(Color.White)
340
- .onClick(() => {
341
- this.isFinished = !this.isFinished;
342
- })
343
- }
344
- }
345
-
346
- @Entry
347
- @Component
348
- struct ForEachIndex {
349
- @State isFinished: boolean = false;
350
- @State planList: ResourceStr[] = [
351
- // 请将$r('app.string.get_up')替换为实际资源文件,在本示例中该资源文件的value值为"7.30 起床"
352
- $r('app.string.get_up'),
353
- // 请将$r('app.string.breakfast')替换为实际资源文件,在本示例中该资源文件的value值为"8.30 早餐"
354
- $r('app.string.breakfast'),
355
- // 请将$r('app.string.lunch')替换为实际资源文件,在本示例中该资源文件的value值为"11.30 中餐"
356
- $r('app.string.lunch'),
357
- // 请将$r('app.string.dinner')替换为实际资源文件,在本示例中该资源文件的value值为"17.30 晚餐"
358
- $r('app.string.dinner'),
359
- // 请将$r('app.string.midnight_snack')替换为实际资源文件,在本示例中该资源文件的value值为"21.30 夜宵"
360
- $r('app.string.midnight_snack'),
361
- // 请将$r('app.string.bathe')替换为实际资源文件,在本示例中该资源文件的value值为"22.30 洗澡"
362
- $r('app.string.bathe'),
363
- // 请将$r('app.string.sleep')替换为实际资源文件,在本示例中该资源文件的value值为"1.30 睡觉"
364
- $r('app.string.sleep')
365
- ];
366
- context1 = this.getUIContext().getHostContext();
367
-
368
- aboutToAppear(): void {
369
- for (let i = 0; i < this.planList.length; i++) {
370
- this.planList[i] = this.context1!.resourceManager.getStringSync((this.planList[i] as Resource).id);
371
- };
372
- }
373
-
374
- build() {
375
- Column() {
376
- // 全部待办
377
- ForEachTodoComponent()
378
-
379
- // 全选
380
- ForEachAllChooseComponent({ isFinished: this.isFinished })
381
-
382
- List() {
383
- ForEach(this.planList, (item: string) => {
384
- // 待办事项1
385
- ForEachThingComponent({ isFinished: this.isFinished, thing: item })
386
- .margin(5)
387
- })
388
- }
389
- }
390
- .height('100%')
391
- .width('100%')
392
- .margin({ top: 5, bottom: 5 })
393
- .backgroundColor('#90f1f3f5')
394
- }
395
- }
396
- ```
397
-
398
- ### @Builder方法
399
-
400
- - Builder方法用于组件内定义方法,可以使得相同代码可以在组件内进行复用。
401
- - 本示例不仅使用了[@Builder](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder)方法进行去重,还对数据进行了移除,可以看到此时代码更加清晰易读,相对于最开始的代码,@Entry组件基本只用于处理页面构建逻辑,而不处理大量与页面设计无关的内容。
402
-
403
- ```typescript
404
- @Observed
405
- class TodoListData {
406
- public planList: ResourceStr[] = [
407
- // 请将$r('app.string.get_up')替换为实际资源文件,在本示例中该资源文件的value值为"7.30 起床"
408
- $r('app.string.get_up'),
409
- // 请将$r('app.string.breakfast')替换为实际资源文件,在本示例中该资源文件的value值为"8.30 早餐"
410
- $r('app.string.breakfast'),
411
- // 请将$r('app.string.lunch')替换为实际资源文件,在本示例中该资源文件的value值为"11.30 中餐"
412
- $r('app.string.lunch'),
413
- // 请将$r('app.string.dinner')替换为实际资源文件,在本示例中该资源文件的value值为"17.30 晚餐"
414
- $r('app.string.dinner'),
415
- // 请将$r('app.string.midnight_snack')替换为实际资源文件,在本示例中该资源文件的value值为"21.30 夜宵"
416
- $r('app.string.midnight_snack'),
417
- // 请将$r('app.string.bathe')替换为实际资源文件,在本示例中该资源文件的value值为"22.30 洗澡"
418
- $r('app.string.bathe'),
419
- // 请将$r('app.string.sleep')替换为实际资源文件,在本示例中该资源文件的value值为"1.30 睡觉"
420
- $r('app.string.sleep')
421
- ];
422
- }
423
-
424
- @Component
425
- struct StateTodoComponent {
426
- build() {
427
- Row() {
428
- // 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
429
- Text($r('app.string.all_tasks'))
430
- .fontSize(30)
431
- .fontWeight(FontWeight.Bold)
432
- }
433
- .width('100%')
434
- .margin({ top: 10, bottom: 10 })
435
- }
436
- }
437
-
438
- @Component
439
- struct BuilderAllChooseComponent {
440
- @Link isFinished: boolean;
441
-
442
- build() {
443
- Row() {
444
- // 请将$r('app.string.check_all')替换为实际资源文件,在本示例中该资源文件的value值为"全选"
445
- Button($r('app.string.check_all'), { type: ButtonType.Capsule })
446
- .onClick(() => {
447
- this.isFinished = !this.isFinished;
448
- })
449
- .fontSize(30)
450
- .fontWeight(FontWeight.Bold)
451
- .backgroundColor('#f7f6cc74')
452
- }
453
- .padding({ left: 15 })
454
- .width('100%')
455
- .margin({ top: 10, bottom: 10 })
456
- }
457
- }
458
-
459
- @Component
460
- struct BuilderThingComponent {
461
- @Prop isFinished: boolean;
462
- @Prop thing: string;
463
-
464
- @Builder
465
- displayIcon(icon: Resource) {
466
- Image(icon)
467
- .width(28)
468
- .height(28)
469
- .onClick(() => {
470
- this.isFinished = !this.isFinished;
471
- })
472
- // ...
473
- }
474
-
475
- build() {
476
- // 待办事项1
477
- Row({ space: 15 }) {
478
- if (this.isFinished) {
479
- // 请将$r('app.media.finished')替换为实际资源文件
480
- this.displayIcon($r('app.media.finished'));
481
- } else {
482
- // 请将$r('app.media.unfinished')替换为实际资源文件
483
- this.displayIcon($r('app.media.unfinished'));
484
- }
485
- Text(`${this.thing}`)
486
- .fontSize(24)
487
- .decoration({ type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None })
488
- .onClick(() => {
489
- // 请将$r('app.string.la_la')替换为实际资源文件,在本示例中该资源文件的value值为"啦"
490
- this.thing += this.getUIContext().getHostContext()!.resourceManager.getStringSync($r('app.string.la_la').id);
491
- })
492
- }
493
- .height('8%')
494
- .width('90%')
495
- .padding({ left: 15 })
496
- .opacity(this.isFinished ? 0.3 : 1)
497
- .border({ width: 1 })
498
- .borderColor(Color.White)
499
- .borderRadius(25)
500
- .backgroundColor(Color.White)
501
- }
502
- }
503
-
504
- @Entry
505
- @Component
506
- struct BuilderIndex {
507
- @State isFinished: boolean = false;
508
- @State data: TodoListData = new TodoListData(); // View绑定ViewModel的数据
509
-
510
- aboutToAppear(): void {
511
- for (let i = 0; i < this.data.planList.length; i++) {
512
- this.data.planList[i] =
513
- this.getUIContext().getHostContext()!.resourceManager.getStringSync((this.data.planList[i] as Resource).id);
514
- }
515
- }
516
-
517
- build() {
518
- Column() {
519
- // 全部待办
520
- StateTodoComponent()
521
-
522
- // 全选
523
- BuilderAllChooseComponent({ isFinished: this.isFinished })
524
-
525
- List() {
526
- ForEach(this.data.planList, (item: string) => {
527
- // 待办事项1
528
- BuilderThingComponent({ isFinished: this.isFinished, thing: item })
529
- .margin(5)
530
- })
531
- }
532
- }
533
- .height('100%')
534
- .width('100%')
535
- .margin({ top: 5, bottom: 5 })
536
- .backgroundColor('#90f1f3f5')
537
- }
538
- }
539
- ```
540
-
541
- ### 总结
542
-
543
- - 通过逐步优化代码结构,可以看到@Entry组件作为页面的入口,其build函数应该仅考虑将需要的组件组合起来,类似于搭积木。被page调用的子组件则如同积木,等着被需要的page进行调用。状态变量类似于粘合剂,当触发UI刷新事件时,状态变量自动刷新绑定的组件,实现page的按需刷新。
544
- - 虽然现有的架构并未使用到MVVM的设计理念,但MVVM的核心理念已初见端倪。ArkUI的UI开发天然适合MVVM模式。在ArkUI中,page和组件构成View层,page负责组织组件,组件则作为构成元素。当组件需要更新时,通过状态变量驱动组件刷新,从而更新page。ViewModel的数据则来源于Model层。
545
- - 示例中的代码功能较为简单,但随着功能的增加,主页面的代码量也会逐渐增多。当备忘录需要添加更多功能,且其他页面也需要使用到主页面的组件时,可以考虑采用MVVM模式来组织项目结构。
546
-
547
- ## 通过MVVM开发备忘录实战
548
-
549
- 上一章节展示了非MVVM模式下的代码组织方式。随着主页面代码的增加,应该采取合理的分层策略,使项目结构清晰,组件之间不互相引用,避免后期维护时牵一发而动全身,增加功能更新的困难。本章将通过对MVVM的核心文件组织模式,向开发者展示如何使用MVVM来重构上一章节的代码。
550
-
551
- ### MVVM文件结构说明
552
-
553
- ```
554
- ├── src
555
- │ ├── ets
556
- │ │ ├── pages 存放页面组件。
557
- │ │ ├── views 存放业务组件。
558
- │ │ ├── shares 存放通用组件。
559
- │ │ └── viewmodel 数据服务。
560
- │ │ ├── LoginViewModel.ets 登录页ViewModel。
561
- │ │ └── xxxViewModel.ets 其他页ViewModel。
562
- ```
563
-
564
- ### 分层设计技巧
565
-
566
- **Model层**
567
-
568
- - model层存放本应用核心数据结构,这层本身和UI开发关系不大,让用户按照自己的业务逻辑进行封装。
569
-
570
- **ViewModel层**
571
-
572
- > **注意:** ViewModel层不只是存放数据,它同时需要提供数据的服务及处理。
573
-
574
- - ViewModel层是为视图服务的数据层。其设计具有两个特点:
575
- 1. 按照页面组织数据。
576
- 2. 每个页面数据进行懒加载。
577
-
578
- **View层**
579
-
580
- View层根据需要来组织,但View层需要区分以下三种组件:
581
-
582
- - **页面组件**:提供整体页面布局,实现多页面之间的跳转,前后台事件处理等页面内容。
583
- - **业务组件**:被页面引用,构建出页面。
584
- - **共享组件**:与项目无关的多项目共享组件。
585
-
586
- 共享组件和业务组件的区别:
587
-
588
- - **业务组件**:包含了ViewModel数据,没有ViewModel,这个组件不能运行。
589
- - **共享组件**:不包含ViewModel层的数据,需要的数据从外部传入。共享组件包含一个自定义组件,只要外部参数(无业务参数)满足,就可以工作。
590
-
591
- ### 代码示例
592
-
593
- 按MVVM模式组织结构,重构如下:
594
-
595
- ```
596
- ├── src
597
- │ ├── ets
598
- │ │ ├── model
599
- │ │ │ ├── ThingModel.ets
600
- │ │ │ └── TodoListModel.ets
601
- │ │ ├── pages
602
- │ │ │ ├── Index.ets
603
- │ │ ├── views
604
- │ │ │ ├── AllChooseComponent.ets
605
- │ │ │ ├── ThingComponent.ets
606
- │ │ │ ├── TodoComponent.ets
607
- │ │ │ └── TodoListComponent.ets
608
- │ │ ├── viewmodel
609
- │ │ │ ├── ThingViewModel.ets
610
- │ │ │ └── TodoListViewModel.ets
611
- │ └── resources
612
- │ ├── rawfile
613
- │ │ ├── default_tasks.json
614
- ```
615
-
616
- 文件代码如下:
617
-
618
- #### ThingModel.ets
619
-
620
- ```typescript
621
- export default class ThingModel {
622
- public thingName: string = 'Todo';
623
- public isFinish: boolean = false;
624
- }
625
- ```
626
-
627
- #### TodoListModel.ets
628
-
629
- ```typescript
630
- import { common } from '@kit.AbilityKit';
631
- import { util } from '@kit.ArkTS';
632
- import { hilog } from '@kit.PerformanceAnalysisKit';
633
- import ThingModel from './ThingModel';
634
-
635
- const DOMAIN = 0x0001;
636
- const TAG = 'TodoListModel';
637
-
638
- export default class TodoListModel {
639
- public things: Array<ThingModel> = [];
640
-
641
- constructor(things: Array<ThingModel>) {
642
- this.things = things;
643
- }
644
-
645
- async loadTasks(context: common.UIAbilityContext) {
646
- try {
647
- let getJson = await context.resourceManager.getRawFileContent('default_tasks.json');
648
- let textDecoderOptions: util.TextDecoderOptions = { ignoreBOM: true };
649
- let textDecoder = util.TextDecoder.create('utf-8', textDecoderOptions);
650
- let result = textDecoder.decodeToString(getJson, { stream: false });
651
- this.things = JSON.parse(result);
652
- } catch (error) {
653
- hilog.error(DOMAIN, TAG, 'Failed to load tasks. Cause: %{public}s', JSON.stringify(error.message));
654
- }
655
- }
656
- }
657
- ```
658
-
659
- #### Index.ets
660
-
661
- ```typescript
662
- import { common } from '@kit.AbilityKit';
663
- // import ViewModel
664
- import TodoListViewModel from '../viewmodel/TodoListViewModel';
665
-
666
- // import View
667
- import { TodoComponent } from '../views/TodoComponent';
668
- import { AllChooseComponent } from '../views/AllChooseComponent';
669
- import { TodoListComponent } from '../views/TodoListComponent';
670
-
671
- @Entry
672
- @Component
673
- struct TodoList {
674
- @State todoListViewModel: TodoListViewModel = new TodoListViewModel(); // View绑定ViewModel的数据
675
- private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
676
-
677
- async aboutToAppear() {
678
- await this.todoListViewModel.loadTasks(this.context);
679
- }
680
-
681
- build() {
682
- Column() {
683
- Row({ space: 40 }) {
684
- // 全部待办
685
- TodoComponent()
686
- // 全选
687
- AllChooseComponent({ todoListViewModel: this.todoListViewModel })
688
- }
689
-
690
- Column() {
691
- TodoListComponent({ thingViewModelArray: this.todoListViewModel.things })
692
- }
693
- }
694
- .height('100%')
695
- .width('100%')
696
- .margin({ top: 5, bottom: 5 })
697
- .backgroundColor('#90f1f3f5')
698
- }
699
- }
700
- ```
701
-
702
- #### AllChooseComponent.ets
703
-
704
- ```typescript
705
- import TodoListViewModel from '../viewmodel/TodoListViewModel';
706
- import { common } from '@kit.AbilityKit';
707
-
708
- @Component
709
- export struct AllChooseComponent {
710
- context1 = this.getUIContext().getHostContext() as common.UIAbilityContext;
711
- // 请在resources\base\element\string.json文件中配置name为'check_all',value为非空字符串的资源
712
- @State titleName: ResourceStr = this.context1.resourceManager.getStringSync($r('app.string.check_all').id);
713
- @Link todoListViewModel: TodoListViewModel;
714
-
715
- build() {
716
- Row() {
717
- Button(`${this.titleName}`, { type: ButtonType.Capsule })
718
- .onClick(() => {
719
- this.todoListViewModel.chooseAll(); // View层点击事件发生时,调用ViewModel层方法chooseAll处理逻辑
720
- this.titleName = this.todoListViewModel.isChosen ?
721
- // 请在resources\base\element\string.json文件中配置name为'check_all',value为非空字符串的资源
722
- this.context1.resourceManager.getStringSync($r('app.string.check_all').id)
723
- // 请在resources\base\element\string.json文件中配置name为'deselect_all',value为非空字符串的资源
724
- : this.context1.resourceManager.getStringSync($r('app.string.deselect_all').id);
725
- })
726
- .fontSize(30)
727
- .fontWeight(FontWeight.Bold)
728
- .backgroundColor('#f7f6cc74')
729
- }
730
- .padding({ left: this.todoListViewModel.isChosen ? 15 : 0 })
731
- .width('100%')
732
- .margin({ top: 10, bottom: 10 })
733
- }
734
- }
735
- ```
736
-
737
- #### ThingComponent.ets
738
-
739
- ```typescript
740
- import ThingViewModel from '../viewmodel/ThingViewModel';
741
-
742
- @Component
743
- export struct ThingComponent {
744
- @ObjectLink thing: ThingViewModel;
745
-
746
- @Builder
747
- displayIcon(icon: Resource) {
748
- Image(icon)
749
- .width(28)
750
- .height(28)
751
- .onClick(() => {
752
- this.thing.updateIsFinish(); // View层点击事件发生时,调用ViewModel层方法updateIsFinish处理逻辑
753
- })
754
- .id(this.thing.thingName)
755
- }
756
-
757
- build() {
758
- // 待办事项
759
- Row({ space: 15 }) {
760
- if (this.thing.isFinish) {
761
- // 请将$r('app.media.finished')替换为实际资源文件
762
- this.displayIcon($r('app.media.finished'));
763
- } else {
764
- // 请将$r('app.media.unfinished')替换为实际资源文件
765
- this.displayIcon($r('app.media.unfinished'));
766
- }
767
-
768
- Text(`${this.thing.thingName}`)
769
- .fontSize(24)
770
- .decoration({ type: this.thing.isFinish ? TextDecorationType.LineThrough : TextDecorationType.None })
771
- .onClick(() => {
772
- this.thing.addSuffixes(); // View层点击事件发生时,调用ViewModel层方法addSuffixes处理逻辑
773
- })
774
- }
775
- .height('8%')
776
- .width('90%')
777
- .padding({ left: 15 })
778
- .opacity(this.thing.isFinish ? 0.3 : 1)
779
- .border({ width: 1 })
780
- .borderColor(Color.White)
781
- .borderRadius(25)
782
- .backgroundColor(Color.White)
783
- }
784
- }
785
- ```
786
-
787
- #### TodoComponent.ets
788
-
789
- ```typescript
790
- @Component
791
- export struct TodoComponent {
792
- build() {
793
- Row() {
794
- // 请将$r('app.string.all_tasks')替换为实际资源文件,在本示例中该资源文件的value值为"全部待办"
795
- Text($r('app.string.all_tasks'))
796
- .fontSize(30)
797
- .fontWeight(FontWeight.Bold)
798
- }
799
- .padding({ left: 15 })
800
- .width('50%')
801
- .margin({ top: 10, bottom: 10 })
802
- }
803
- }
804
- ```
805
-
806
- #### TodoListComponent.ets
807
-
808
- ```typescript
809
- import ThingViewModel from '../viewmodel/ThingViewModel';
810
- import { ThingViewModelArray } from '../viewmodel/TodoListViewModel'
811
- import { ThingComponent } from './ThingComponent';
812
-
813
- @Component
814
- export struct TodoListComponent {
815
- @ObjectLink thingViewModelArray: ThingViewModelArray;
816
-
817
- build() {
818
- Column() {
819
- List() {
820
- ForEach(this.thingViewModelArray, (item: ThingViewModel) => {
821
- // 待办事项
822
- ListItem() {
823
- ThingComponent({ thing: item })
824
- .margin(5)
825
- }
826
- }, (item: ThingViewModel) => {
827
- return item.thingName;
828
- })
829
- }
830
- }
831
- }
832
- }
833
- ```
834
-
835
- #### ThingViewModel.ets
836
-
837
- ```typescript
838
- import ThingModel from '../model/ThingModel';
839
-
840
- @Observed
841
- export default class ThingViewModel {
842
- @Track public thingName: string = 'Todo';
843
- @Track public isFinish: boolean = false;
844
- public context: Context = AppStorage.get('context')!;
845
-
846
- updateTask(thing: ThingModel) {
847
- this.thingName = thing.thingName;
848
- this.isFinish = thing.isFinish;
849
- }
850
-
851
- updateIsFinish(): void {
852
- this.isFinish = !this.isFinish;
853
- }
854
-
855
- addSuffixes(): void {
856
- // 请在resources\base\element\string.json文件中配置name为'la_la',value为非空字符串的资源
857
- this.thingName += this.context.resourceManager.getStringSync($r('app.string.la_la').id);
858
- }
859
- }
860
- ```
861
-
862
- #### TodoListViewModel.ets
863
-
864
- ```typescript
865
- import ThingViewModel from './ThingViewModel';
866
- import { common } from '@kit.AbilityKit';
867
- import TodoListModel from '../model/TodoListModel';
868
-
869
- @Observed
870
- export class ThingViewModelArray extends Array<ThingViewModel> {
871
- }
872
-
873
- @Observed
874
- export default class TodoListViewModel {
875
- @Track public isChosen: boolean = true;
876
- @Track public things: ThingViewModelArray = new ThingViewModelArray();
877
-
878
- async loadTasks(context: common.UIAbilityContext) {
879
- let todoList = new TodoListModel([]);
880
- await todoList.loadTasks(context);
881
- for (let thing of todoList.things) {
882
- let todoListViewModel = new ThingViewModel();
883
- todoListViewModel.updateTask(thing);
884
- this.things.push(todoListViewModel);
885
- }
886
- }
887
-
888
- chooseAll(): void {
889
- for (let thing of this.things) {
890
- thing.isFinish = this.isChosen;
891
- }
892
- this.isChosen = !this.isChosen;
893
- }
894
- }
895
- ```
896
-
897
- #### default_tasks.json
898
-
899
- ```json
900
- [
901
- {"thingName": "7.30起床", "isFinish": false},
902
- {"thingName": "8.30早餐", "isFinish": false},
903
- {"thingName": "11.30中餐", "isFinish": false},
904
- {"thingName": "17.30晚餐", "isFinish": false},
905
- {"thingName": "21.30夜宵", "isFinish": false},
906
- {"thingName": "22.30洗澡", "isFinish": false},
907
- {"thingName": "1.30睡觉", "isFinish": false}
908
- ]
909
- ```
910
-
911
- MVVM模式拆分后的代码结构更加清晰,模块职责更明确。新页面需要使用事件组件,比如TodoListComponent组件,只需导入组件。