【Compose Multiplatform跨端声明式UI实用技术】第3课:Compose Hot Reload 与开发效率工具链

第2课完成了项目搭建。本课将深入 CMP 开发效率的核心工具链:Compose Hot Reload 的完整工作流、统一 @Preview 注解在 commonMain 中的使用技巧、MCP 服务器与 AI 编码助手的集成方式,以及语义树调试等效率工具。本课内容基于 CMP 1.12.1。 文末附有15道配套习题与本课知识点总结。

目录

  • [一、Compose Hot Reload:从 1.0 到 MCP](#一、Compose Hot Reload:从 1.0 到 MCP)
    • [1.1 什么是 Compose Hot Reload](#1.1 什么是 Compose Hot Reload)
    • [1.2 版本演进:从实验到稳定](#1.2 版本演进:从实验到稳定)
    • [1.3 项目配置](#1.3 项目配置)
    • [1.4 运行与触发方式](#1.4 运行与触发方式)
  • [二、统一 @Preview 注解:告别三套注解](#二、统一 @Preview 注解:告别三套注解)
    • [2.1 背景与变化](#2.1 背景与变化)
    • [2.2 配置方法](#2.2 配置方法)
    • [2.3 @Preview 注解参数](#2.3 @Preview 注解参数)
    • [2.4 Hot Reload 与 Preview 的定位差异](#2.4 Hot Reload 与 Preview 的定位差异)
  • [三、MCP 服务器与 AI 编码助手集成](#三、MCP 服务器与 AI 编码助手集成)
    • [3.1 MCP 服务器概述](#3.1 MCP 服务器概述)
    • [3.2 MCP 服务器提供的工具](#3.2 MCP 服务器提供的工具)
    • [3.3 连接配置](#3.3 连接配置)
  • 四、语义树调试
    • [4.1 什么是语义树](#4.1 什么是语义树)
    • [4.2 打印语义树](#4.2 打印语义树)
    • [4.3 语义树在调试中的应用](#4.3 语义树在调试中的应用)
  • [五、IDE 工具链与调试](#五、IDE 工具链与调试)
    • [5.1 Kotlin Multiplatform IDE 插件](#5.1 Kotlin Multiplatform IDE 插件)
    • [5.2 断点调试](#5.2 断点调试)
    • [5.3 布局检查器](#5.3 布局检查器)
  • 六、已知限制与最佳实践
    • [6.1 Hot Reload 的已知限制](#6.1 Hot Reload 的已知限制)
    • [6.2 最佳实践总结](#6.2 最佳实践总结)
  • 配套习题(15题)
  • 本课知识点总结

一、Compose Hot Reload:从 1.0 到 MCP

1.1 什么是 Compose Hot Reload

Compose Hot Reload 帮助你在开发 Compose Multiplatform 项目时,实时查看并尝试 UI 更改。与标准的 Compose 预览(适用于使用测试数据查看独立组件)不同,Compose Hot Reload 会直接将你的代码更改应用到运行中的应用程序。

捆绑的 Compose Hot Reload Gradle 插件需要 Kotlin 2.1.20+ 以及与 Java 21 或更早版本兼容的 JVM 目标。为了使用 Compose Hot Reload 的完整功能,建议安装 Kotlin Multiplatform IDE 插件,该插件适用于 IntelliJ IDEA 2025.2.2 及以上版本,以及 Android Studio Otter 2025.2.1 及以上版本。

1.2 版本演进:从实验到稳定

Compose Hot Reload 在 CMP 1.10.0 中达到稳定状态(1.0.0),Compose Hot Reload 插件现已稳定,并随 Compose Multiplatform Gradle 插件捆绑,默认启用,无需额外配置。

CMP 1.12.0 进一步引入了实验性的 MCP 服务器。在此之前,当 AI 代理编辑 Compose 代码时,没有可靠的方法来验证结果------代理无法确认热重载是否成功,无法看到渲染后的 UI,也无法读取运行时日志或异常。通过 MCP 服务器,代理可以触发重载、截取屏幕截图、检查语义树、模拟点击和文本输入,并读取应用程序日志,从而验证自身编辑的结果,无需人工干预。

下面是 Compose Hot Reload 的版本演进时间线:
#mermaid-svg-4bH960fAbxd5X4sh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4bH960fAbxd5X4sh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4bH960fAbxd5X4sh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4bH960fAbxd5X4sh .error-icon{fill:#552222;}#mermaid-svg-4bH960fAbxd5X4sh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4bH960fAbxd5X4sh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4bH960fAbxd5X4sh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4bH960fAbxd5X4sh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4bH960fAbxd5X4sh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4bH960fAbxd5X4sh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4bH960fAbxd5X4sh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4bH960fAbxd5X4sh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4bH960fAbxd5X4sh .marker.cross{stroke:#333333;}#mermaid-svg-4bH960fAbxd5X4sh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4bH960fAbxd5X4sh p{margin:0;}#mermaid-svg-4bH960fAbxd5X4sh .edge{stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .section--1 rect,#mermaid-svg-4bH960fAbxd5X4sh .section--1 path,#mermaid-svg-4bH960fAbxd5X4sh .section--1 circle,#mermaid-svg-4bH960fAbxd5X4sh .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section--1 text{fill:#ffffff;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth--1{stroke-width:17;}#mermaid-svg-4bH960fAbxd5X4sh .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:#ffffff;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-0 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-0 path,#mermaid-svg-4bH960fAbxd5X4sh .section-0 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-4bH960fAbxd5X4sh .section-0 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-0{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-0{stroke-width:14;}#mermaid-svg-4bH960fAbxd5X4sh .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-1 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-1 path,#mermaid-svg-4bH960fAbxd5X4sh .section-1 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-1 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-1{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-1{stroke-width:11;}#mermaid-svg-4bH960fAbxd5X4sh .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-2 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-2 path,#mermaid-svg-4bH960fAbxd5X4sh .section-2 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-2 text{fill:#ffffff;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-2{stroke-width:8;}#mermaid-svg-4bH960fAbxd5X4sh .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:#ffffff;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-3 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-3 path,#mermaid-svg-4bH960fAbxd5X4sh .section-3 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-3 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-3{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-3{stroke-width:5;}#mermaid-svg-4bH960fAbxd5X4sh .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-4 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-4 path,#mermaid-svg-4bH960fAbxd5X4sh .section-4 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-4 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-4{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-4{stroke-width:2;}#mermaid-svg-4bH960fAbxd5X4sh .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-5 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-5 path,#mermaid-svg-4bH960fAbxd5X4sh .section-5 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-5 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-5{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-5{stroke-width:-1;}#mermaid-svg-4bH960fAbxd5X4sh .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-6 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-6 path,#mermaid-svg-4bH960fAbxd5X4sh .section-6 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-6 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-6{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-6{stroke-width:-4;}#mermaid-svg-4bH960fAbxd5X4sh .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-7 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-7 path,#mermaid-svg-4bH960fAbxd5X4sh .section-7 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-7 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-7{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-7{stroke-width:-7;}#mermaid-svg-4bH960fAbxd5X4sh .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-8 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-8 path,#mermaid-svg-4bH960fAbxd5X4sh .section-8 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-8 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-8{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-8{stroke-width:-10;}#mermaid-svg-4bH960fAbxd5X4sh .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-9 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-9 path,#mermaid-svg-4bH960fAbxd5X4sh .section-9 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-9 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-9{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-9{stroke-width:-13;}#mermaid-svg-4bH960fAbxd5X4sh .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-10 rect,#mermaid-svg-4bH960fAbxd5X4sh .section-10 path,#mermaid-svg-4bH960fAbxd5X4sh .section-10 circle,#mermaid-svg-4bH960fAbxd5X4sh .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-10 text{fill:black;}#mermaid-svg-4bH960fAbxd5X4sh .node-icon-10{font-size:40px;color:black;}#mermaid-svg-4bH960fAbxd5X4sh .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .edge-depth-10{stroke-width:-16;}#mermaid-svg-4bH960fAbxd5X4sh .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-4bH960fAbxd5X4sh .lineWrapper line{stroke:black;}#mermaid-svg-4bH960fAbxd5X4sh .disabled,#mermaid-svg-4bH960fAbxd5X4sh .disabled circle,#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:lightgray;}#mermaid-svg-4bH960fAbxd5X4sh .disabled text{fill:#efefef;}#mermaid-svg-4bH960fAbxd5X4sh .section-root rect,#mermaid-svg-4bH960fAbxd5X4sh .section-root path,#mermaid-svg-4bH960fAbxd5X4sh .section-root circle{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-4bH960fAbxd5X4sh .section-root text{fill:#ffffff;}#mermaid-svg-4bH960fAbxd5X4sh .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-4bH960fAbxd5X4sh .edge{fill:none;}#mermaid-svg-4bH960fAbxd5X4sh .eventWrapper{filter:brightness(120%);}#mermaid-svg-4bH960fAbxd5X4sh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CMP 1.10.0 Hot Reload 达到稳定状态(1.0.0) 插件默认捆绑,无需额外配置 CMP 1.12.0 引入实验性 MCP 服务器 AI 代理可验证编辑结果 CMP 1.12.1 本课内容基于的版本 Compose Hot Reload 版本演进

1.3 项目配置

新项目:在 IDE 中通过 Kotlin Multiplatform 向导创建项目时,勾选 Desktop 目标,Compose Hot Reload 自动添加。

现有项目:从 Compose Multiplatform 1.10.0 开始,如果你的项目已经包含桌面目标,升级到 1.10.0 或更高版本即可开箱即用体验 Compose Hot Reload 功能。

旧版本项目:对于使用 1.10.0 之前版本的 Compose Multiplatform 的多平台项目,你必须配置桌面目标,然后显式添加 Compose Hot Reload 插件。

1.4 运行与触发方式

IDE 内运行 :在运行配置下拉中选择 desktopApp [hot] 🔥,或点击主函数旁的 Run 图标直接从 IDE gutter 运行。

命令行运行 :使用 ./gradlew :desktopApp:hotRun --auto 启用自动重载模式。

手动触发:按指定快捷键或点击 Reload UI 按钮显式触发热重载。触发行为可在 Settings | Tools | Compose Hot Reload 页面修改。

自动模式 :在 CLI 或运行配置设置中指定 --autoReload 或 --auto 参数即可启用。编辑的每次更改会在保存时自动应用。

二、统一 @Preview 注解:告别三套注解

2.1 背景与变化

Compose Multiplatform 最初将受限的 @Preview 注解实现为一个自定义库。此前,开发者需要维护三套不同的 @Preview 注解,分布在不同包中,这给跨平台开发带来了困扰------很难确定注解、平台和 IDE 之间的正确组合。

CMP 1.10.0 将预览统一在单一 @Preview 注解下,该注解可在 commonMain 源集中工作。所有其他注解已弃用,IDE 快速修复建议会帮助你将依赖项更新为 androidx.compose.ui.tooling.preview.Preview。

2.2 配置方法

要在共享代码中使用预览功能,需要在 build.gradle.kts 文件中添加必要的依赖项:

kotlin 复制代码
kotlin {
    sourceSets {
        commonMain.dependencies {
            // 新注解,CMP 1.10.0 及更高版本可用
            implementation("org.jetbrains.compose.ui:ui-tooling-preview:1.10.0")
            // 导入新注解:
            // import androidx.compose.ui.tooling.preview.Preview
        }
    }
}

在 common 代码模块的根 dependencies {} 块中声明工具依赖。如果使用 com.android.application 或 com.android.library 插件,则添加:debugImplementation("org.jetbrains.compose.ui:ui-tooling:1.10.0")。

一个重要提醒:要在 Kotlin Multiplatform 项目的公共代码中启用 Compose 预览,你需要一个 Android 目标,因为预览依赖 Android 库。

2.3 @Preview 注解参数

Compose Multiplatform 支持以下参数来配置预览的呈现方式:

  • name:预览的显示名称
  • group:预览的分组名称,支持对相关预览进行逻辑组织和选择性显示
  • widthDp:最大宽度(单位 dp)
  • heightDp:最大高度(单位 dp)
  • locale:预览的语言环境
  • showBackground:是否显示背景
  • backgroundColor:背景颜色

这些参数在 IntelliJ IDEA 和 Android Studio 中均被识别并生效。它们让你可以在一个文件中预览同一组件的不同状态(如暗色模式、不同语言、不同屏幕尺寸),无需切换运行环境。

2.4 Hot Reload 与 Preview 的定位差异

Preview 和 Hot Reload 是互补关系,而非替代关系。Preview 适合隔离组件 的快速查看------给组件传入测试数据,在不同配置下渲染,验证组件的视觉表现。Hot Reload 适合完整应用上下文中的 UI 迭代------在真实的应用状态、导航栈和数据流中调整界面。两者配合使用:用 Preview 快速验证组件设计,用 Hot Reload 验证组件在实际应用中的表现。

下面是 Preview 与 Hot Reload 的定位差异对比:
#mermaid-svg-7iaRc22pMzUuZM4I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7iaRc22pMzUuZM4I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7iaRc22pMzUuZM4I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7iaRc22pMzUuZM4I .error-icon{fill:#552222;}#mermaid-svg-7iaRc22pMzUuZM4I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7iaRc22pMzUuZM4I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7iaRc22pMzUuZM4I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7iaRc22pMzUuZM4I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7iaRc22pMzUuZM4I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7iaRc22pMzUuZM4I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7iaRc22pMzUuZM4I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7iaRc22pMzUuZM4I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7iaRc22pMzUuZM4I .marker.cross{stroke:#333333;}#mermaid-svg-7iaRc22pMzUuZM4I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7iaRc22pMzUuZM4I p{margin:0;}#mermaid-svg-7iaRc22pMzUuZM4I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7iaRc22pMzUuZM4I .cluster-label text{fill:#333;}#mermaid-svg-7iaRc22pMzUuZM4I .cluster-label span{color:#333;}#mermaid-svg-7iaRc22pMzUuZM4I .cluster-label span p{background-color:transparent;}#mermaid-svg-7iaRc22pMzUuZM4I .label text,#mermaid-svg-7iaRc22pMzUuZM4I span{fill:#333;color:#333;}#mermaid-svg-7iaRc22pMzUuZM4I .node rect,#mermaid-svg-7iaRc22pMzUuZM4I .node circle,#mermaid-svg-7iaRc22pMzUuZM4I .node ellipse,#mermaid-svg-7iaRc22pMzUuZM4I .node polygon,#mermaid-svg-7iaRc22pMzUuZM4I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7iaRc22pMzUuZM4I .rough-node .label text,#mermaid-svg-7iaRc22pMzUuZM4I .node .label text,#mermaid-svg-7iaRc22pMzUuZM4I .image-shape .label,#mermaid-svg-7iaRc22pMzUuZM4I .icon-shape .label{text-anchor:middle;}#mermaid-svg-7iaRc22pMzUuZM4I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7iaRc22pMzUuZM4I .rough-node .label,#mermaid-svg-7iaRc22pMzUuZM4I .node .label,#mermaid-svg-7iaRc22pMzUuZM4I .image-shape .label,#mermaid-svg-7iaRc22pMzUuZM4I .icon-shape .label{text-align:center;}#mermaid-svg-7iaRc22pMzUuZM4I .node.clickable{cursor:pointer;}#mermaid-svg-7iaRc22pMzUuZM4I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7iaRc22pMzUuZM4I .arrowheadPath{fill:#333333;}#mermaid-svg-7iaRc22pMzUuZM4I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7iaRc22pMzUuZM4I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7iaRc22pMzUuZM4I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7iaRc22pMzUuZM4I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7iaRc22pMzUuZM4I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7iaRc22pMzUuZM4I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7iaRc22pMzUuZM4I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7iaRc22pMzUuZM4I .cluster text{fill:#333;}#mermaid-svg-7iaRc22pMzUuZM4I .cluster span{color:#333;}#mermaid-svg-7iaRc22pMzUuZM4I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7iaRc22pMzUuZM4I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7iaRc22pMzUuZM4I rect.text{fill:none;stroke-width:0;}#mermaid-svg-7iaRc22pMzUuZM4I .icon-shape,#mermaid-svg-7iaRc22pMzUuZM4I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7iaRc22pMzUuZM4I .icon-shape p,#mermaid-svg-7iaRc22pMzUuZM4I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7iaRc22pMzUuZM4I .icon-shape .label rect,#mermaid-svg-7iaRc22pMzUuZM4I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7iaRc22pMzUuZM4I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7iaRc22pMzUuZM4I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7iaRc22pMzUuZM4I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Hot Reload 应用级验证
完整应用上下文
真实状态与导航栈
数据流中调整界面
Preview 组件级验证
隔离组件
传入测试数据
不同配置渲染
快速验证组件设计
验证组件实际表现
两者配合使用

三、MCP 服务器与 AI 编码助手集成

3.1 MCP 服务器概述

从 CMP 1.12.0 开始,Compose Hot Reload 附带了一个实验性的 Model Context Protocol(MCP)服务器,允许 AI 编码代理直接与运行中的 Compose 应用程序进行交互。MCP 服务器连接到热重载编排层,可以通过 hotMcpServer Gradle 任务启动。它可以在没有运行中的应用时启动,会轮询等待应用、检测关闭并自动重连。

在实践中,这意味着代理可以验证自身编辑的结果:确认重载是否成功、检查渲染后的 UI、捕获运行时异常,然后迭代------所有这些都无需人工描述屏幕上的内容。

3.2 MCP 服务器提供的工具

MCP 服务器暴露以下工具:

工具 描述
status 获取运行中应用的当前状态
take_screenshot 截取应用 UI 的屏幕截图
get_semantic_tree 检查带语义信息的 UI 组件树
click 点击 UI 元素
long_click 长按 UI 元素
type_text 替换文本字段的内容
scroll 在可滚动组件中按像素滚动
scroll_to_index 在可滚动组件中按项滚动

这些工具让 AI 代理能够形成完整的验证闭环:编辑代码 → 触发热重载 → 截取屏幕截图 → 检查语义树 → 模拟用户交互 → 读取日志确认无异常。

下面是 AI 代理通过 MCP 服务器形成的验证闭环:
#mermaid-svg-Zppcx4GBMSV8O5xQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Zppcx4GBMSV8O5xQ .error-icon{fill:#552222;}#mermaid-svg-Zppcx4GBMSV8O5xQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Zppcx4GBMSV8O5xQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .marker.cross{stroke:#333333;}#mermaid-svg-Zppcx4GBMSV8O5xQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Zppcx4GBMSV8O5xQ p{margin:0;}#mermaid-svg-Zppcx4GBMSV8O5xQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .cluster-label text{fill:#333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .cluster-label span{color:#333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .cluster-label span p{background-color:transparent;}#mermaid-svg-Zppcx4GBMSV8O5xQ .label text,#mermaid-svg-Zppcx4GBMSV8O5xQ span{fill:#333;color:#333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .node rect,#mermaid-svg-Zppcx4GBMSV8O5xQ .node circle,#mermaid-svg-Zppcx4GBMSV8O5xQ .node ellipse,#mermaid-svg-Zppcx4GBMSV8O5xQ .node polygon,#mermaid-svg-Zppcx4GBMSV8O5xQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Zppcx4GBMSV8O5xQ .rough-node .label text,#mermaid-svg-Zppcx4GBMSV8O5xQ .node .label text,#mermaid-svg-Zppcx4GBMSV8O5xQ .image-shape .label,#mermaid-svg-Zppcx4GBMSV8O5xQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-Zppcx4GBMSV8O5xQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Zppcx4GBMSV8O5xQ .rough-node .label,#mermaid-svg-Zppcx4GBMSV8O5xQ .node .label,#mermaid-svg-Zppcx4GBMSV8O5xQ .image-shape .label,#mermaid-svg-Zppcx4GBMSV8O5xQ .icon-shape .label{text-align:center;}#mermaid-svg-Zppcx4GBMSV8O5xQ .node.clickable{cursor:pointer;}#mermaid-svg-Zppcx4GBMSV8O5xQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .arrowheadPath{fill:#333333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Zppcx4GBMSV8O5xQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zppcx4GBMSV8O5xQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Zppcx4GBMSV8O5xQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zppcx4GBMSV8O5xQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Zppcx4GBMSV8O5xQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Zppcx4GBMSV8O5xQ .cluster text{fill:#333;}#mermaid-svg-Zppcx4GBMSV8O5xQ .cluster span{color:#333;}#mermaid-svg-Zppcx4GBMSV8O5xQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Zppcx4GBMSV8O5xQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Zppcx4GBMSV8O5xQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-Zppcx4GBMSV8O5xQ .icon-shape,#mermaid-svg-Zppcx4GBMSV8O5xQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zppcx4GBMSV8O5xQ .icon-shape p,#mermaid-svg-Zppcx4GBMSV8O5xQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Zppcx4GBMSV8O5xQ .icon-shape .label rect,#mermaid-svg-Zppcx4GBMSV8O5xQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zppcx4GBMSV8O5xQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Zppcx4GBMSV8O5xQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Zppcx4GBMSV8O5xQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 验证通过
发现问题
AI 代理编辑代码
触发热重载
截取屏幕截图
检查语义树
模拟用户交互
读取日志确认无异常
迭代完成

3.3 连接配置

在项目根目录的 .mcp.json 文件中配置:

json 复制代码
{
  "mcpServers": {
    "compose-hot-reload": {
      "command": "./gradlew",
      "args": ["--no-daemon", "--quiet", "--console=plain", "hotMcpServer"]
    }
  }
}

注意事项 :MCP 服务器目前处于实验状态。另外,MCP 服务器使用的是 ./gradlew --no-daemon,它是在构建守护进程和应用 JVM 之外的第二个 Gradle JVM,在资源受限的环境中需要注意内存占用。

四、语义树调试

4.1 什么是语义树

Compose 的语义树(Semantics Tree)是 UI 组件树的结构化表示,每个节点携带语义信息(如文本内容、角色、状态、测试标签等)。语义树是 UI 测试和辅助功能的基础,也是调试 UI 问题的重要工具。

4.2 打印语义树

在测试中,使用 printToLog() 输出语义树:

kotlin 复制代码
composeTestRule.onRoot().printToLog("TAG")

这会输出类似以下的日志:

复制代码
Node #1 at (...)px
 |-Node #2 at (...)px
   ...
   MergeDescendants = 'true'

printToLog() 函数将语义节点信息打印到日志中(debug 级别)。默认情况下会打印整个子层级,可以通过设置 maxDepth 参数来控制打印的深度。需要注意的是,该函数会获取它之前收集的 ID 的最新节点快照,因此如果树发生变化,输出也会随之改变。

如果需要查看未被合并的完整语义树,使用 useUnmergedTree = true:

kotlin 复制代码
composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")

4.3 语义树在调试中的应用

语义树在以下场景中尤其有用:

定位测试失败原因。当测试中出现 "no node matched" 或 "found N nodes" 错误时,打印语义树可以查看实际的节点结构,检查匹配条件是否正确。

验证 UI 结构。语义树展示了组件的层级关系和属性,可以帮助你确认 Compose 的布局结构是否符合预期。

AI 代理的 UI 理解 。MCP 服务器的 get_semantic_tree 工具让 AI 代理能够获取运行中应用的 UI 结构,从而理解当前界面状态并做出正确的交互决策。

下面是语义树在调试中的主要应用场景:
#mermaid-svg-swCUvgeQ4QPWVLB3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-swCUvgeQ4QPWVLB3 .error-icon{fill:#552222;}#mermaid-svg-swCUvgeQ4QPWVLB3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-swCUvgeQ4QPWVLB3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .marker.cross{stroke:#333333;}#mermaid-svg-swCUvgeQ4QPWVLB3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-swCUvgeQ4QPWVLB3 p{margin:0;}#mermaid-svg-swCUvgeQ4QPWVLB3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .cluster-label text{fill:#333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .cluster-label span{color:#333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .cluster-label span p{background-color:transparent;}#mermaid-svg-swCUvgeQ4QPWVLB3 .label text,#mermaid-svg-swCUvgeQ4QPWVLB3 span{fill:#333;color:#333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .node rect,#mermaid-svg-swCUvgeQ4QPWVLB3 .node circle,#mermaid-svg-swCUvgeQ4QPWVLB3 .node ellipse,#mermaid-svg-swCUvgeQ4QPWVLB3 .node polygon,#mermaid-svg-swCUvgeQ4QPWVLB3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-swCUvgeQ4QPWVLB3 .rough-node .label text,#mermaid-svg-swCUvgeQ4QPWVLB3 .node .label text,#mermaid-svg-swCUvgeQ4QPWVLB3 .image-shape .label,#mermaid-svg-swCUvgeQ4QPWVLB3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-swCUvgeQ4QPWVLB3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-swCUvgeQ4QPWVLB3 .rough-node .label,#mermaid-svg-swCUvgeQ4QPWVLB3 .node .label,#mermaid-svg-swCUvgeQ4QPWVLB3 .image-shape .label,#mermaid-svg-swCUvgeQ4QPWVLB3 .icon-shape .label{text-align:center;}#mermaid-svg-swCUvgeQ4QPWVLB3 .node.clickable{cursor:pointer;}#mermaid-svg-swCUvgeQ4QPWVLB3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .arrowheadPath{fill:#333333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-swCUvgeQ4QPWVLB3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-swCUvgeQ4QPWVLB3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-swCUvgeQ4QPWVLB3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-swCUvgeQ4QPWVLB3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-swCUvgeQ4QPWVLB3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-swCUvgeQ4QPWVLB3 .cluster text{fill:#333;}#mermaid-svg-swCUvgeQ4QPWVLB3 .cluster span{color:#333;}#mermaid-svg-swCUvgeQ4QPWVLB3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-swCUvgeQ4QPWVLB3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-swCUvgeQ4QPWVLB3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-swCUvgeQ4QPWVLB3 .icon-shape,#mermaid-svg-swCUvgeQ4QPWVLB3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-swCUvgeQ4QPWVLB3 .icon-shape p,#mermaid-svg-swCUvgeQ4QPWVLB3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-swCUvgeQ4QPWVLB3 .icon-shape .label rect,#mermaid-svg-swCUvgeQ4QPWVLB3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-swCUvgeQ4QPWVLB3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-swCUvgeQ4QPWVLB3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-swCUvgeQ4QPWVLB3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 语义树调试
定位测试失败原因
验证 UI 结构
AI 代理 UI 理解
检查节点匹配条件
确认层级关系与属性
获取运行中 UI 结构
做出交互决策

五、IDE 工具链与调试

5.1 Kotlin Multiplatform IDE 插件

Kotlin Multiplatform IDE 插件是 CMP 开发效率的核心支撑,提供以下功能:

  • Compose Multiplatform 资源支持:多平台资源的自动化处理
  • UI 预览支持:在 commonMain 中的 @Preview 注解支持,直接在 IDE 中预览组件
  • Compose Hot Reload 集成:自动检测热重载运行配置,即时查看 UI 更改
  • 自动生成的运行配置:为所有支持的平台生成运行配置,iOS 和 Android 有设备选择器
  • 跨语言调试:Swift 和 Kotlin 的跨语言导航和调试,Swift 语法高亮和快速文档

该插件兼容 IntelliJ IDEA 和 Android Studio:IntelliJ IDEA 在 macOS 上从 2025.1.1.1 版本开始支持,在 Windows 和 Linux 上从 2025.2.2 版本开始支持。Android Studio 在 macOS 上从 Narwhal 2025.1.1 版本开始支持,在 Windows 和 Linux 上从 Otter 2025.2.1 版本开始支持。

5.2 断点调试

Android 和 Desktop:在 IDE 中直接设置断点,以 Debug 模式运行应用即可。

Web(Wasm) :调试 Kotlin/Wasm 代码在 IntelliJ IDEA 2025.3 版本中可用(EAP 阶段)。需要在 IntelliJ IDEA 中打开 Kotlin 文件,点击行号设置断点,在运行配置列表中选择 composeApp[wasmJs],点击调试图标运行。需要安装 JavaScript Debugger 插件。

应用启动后会在浏览器中打开,Debug 面板自动出现在 IntelliJ IDEA 中。你可以在断点处检查变量、单步执行代码、查看线程和变量面板来追踪函数调用序列。

5.3 布局检查器

在 Android Studio 中,布局检查器(Layout Inspector)可以用于调试 CMP 应用的布局。它可以显示 Compose 可组合项的重组计数,帮助你识别性能问题。需要注意的是,布局检查器目前仅在运行 Android 目标时可用,Desktop、iOS 和 Web 目标的布局检查器尚未实现。

六、已知限制与最佳实践

6.1 Hot Reload 的已知限制

窗口闪烁问题 :开发工具窗口自动吸附到主应用程序窗口,在某些窗口管理器上可能导致异常行为(闪烁、跳动等)。解决方法:设置 -Dcompose.reload.devToolsDetached=true 以分离模式运行开发工具窗口,设置 -Dcompose.reload.devToolsHeadless=true 以无头模式运行,或设置 -Dcompose.reload.devToolsEnabled=false 完全禁用开发工具进程。

虚拟桌面问题:所有主流操作系统都支持某种形式的虚拟桌面,但它们不提供访问窗口当前桌面信息或更改窗口虚拟桌面的公共 API。Compose Hot Reload 无法可靠检测用户窗口在哪个虚拟桌面上生成,也无法在必要时更改开发工具窗口的虚拟桌面。建议在虚拟桌面环境中以分离模式运行开发工具窗口。

Windows Dev Drive(ReFS)不兼容:Compose Hot Reload 依赖 Gradle 文件系统监视来检测源文件更改,但 Gradle 的文件系统监视目前不支持 ReFS 文件系统。建议将项目移动到 NTFS 格式的驱动器,同时可以将 .gradle 缓存保留在 Dev Drive 上以获得构建性能优势。

多应用连接限制:Kotlin Multiplatform IDE 插件目前仅支持单个 Compose Hot Reload 连接。如果在同一 IDE 实例中同时运行多个 Compose 应用程序,可能会遇到意外关闭。仅支持单个连接是刻意设计。

不支持调试模式 :使用 hotRun 以调试模式运行并设置断点时,断点会被忽略。这是当前版本的已知限制。

6.2 最佳实践总结

将 Desktop 目标作为开发沙盒。即使不发布桌面应用,Desktop 目标也提供了最快的 UI 迭代路径。Hot Reload 依赖 JVM 目标,桌面窗口让你可以在 commonMain 中修改 UI 后立即看到效果。

Preview 与 Hot Reload 配合使用。Preview 用于组件级设计验证,Hot Reload 用于应用级功能验证。不要试图用其中一个替代另一个。

AI 辅助开发时启用 MCP 服务器。如果使用 AI 编码助手,配置 MCP 服务器让代理能够自我验证编辑结果。代理可以在每次编辑后自动触发重载、截图并检查语义树,减少了人工验证的工作量。

遇到窗口闪烁时切换到分离模式 。在多显示器、虚拟桌面或特定窗口管理器环境下,设置 -Dcompose.reload.devToolsDetached=true 可以避免开发工具窗口的异常行为。

配套习题(15题)

第一组:Compose Hot Reload 基础(4题)

第1题 Compose Hot Reload 在哪个版本达到稳定状态并默认捆绑在 Gradle 插件中?( )

A. CMP 1.8.0

B. CMP 1.9.0

C. CMP 1.10.0

D. CMP 1.12.0

答案:C

解析:CMP 1.10.0 将 Compose Hot Reload 带入稳定状态,Compose Hot Reload 插件现已稳定,并随 Compose Multiplatform Gradle 插件捆绑,默认启用,无需额外配置。捆绑的 Compose Hot Reload Gradle 插件需要 Kotlin 2.1.20+ 以及与 Java 21 或更早版本兼容的 JVM 目标。

第2题 Compose Hot Reload 与标准 Compose Preview 的核心区别是( )

A. Hot Reload 只能在 Android 上运行,Preview 跨平台

B. Hot Reload 直接将代码更改应用到运行中的应用程序,Preview 用于查看孤立组件的测试数据

C. Hot Reload 需要重启应用,Preview 不需要

D. 两者功能完全相同,可以互相替代

答案:B

解析:Compose Hot Reload 会直接将你的代码更改应用到运行中的应用程序,而标准的 Compose 预览适用于使用测试数据查看独立组件。两者是互补关系:Preview 适合组件级设计验证,Hot Reload 适合应用级功能验证。

第3题 使用 Compose Hot Reload 的必要条件不包括( )

A. Kotlin 2.1.20 或更高版本

B. 与 Java 21 或更早版本兼容的 JVM 目标

C. Desktop 目标

D. Android 目标

答案:D

解析:Compose Hot Reload 需要 Kotlin 2.1.20+、与 Java 21 或更早版本兼容的 JVM 目标,以及 Desktop 目标。Android 目标不是 Hot Reload 的必要条件。Hot Reload 依赖 JVM 目标,桌面目标是其运行的基础。

第4题 从 CMP 1.10.0 开始,现有项目要启用 Compose Hot Reload,需要( )

A. 手动添加 Compose Hot Reload Gradle 插件

B. 升级到 CMP 1.10.0 或更高版本,如果项目已包含桌面目标即可开箱即用

C. 安装独立的 Hot Reload 应用程序

D. 在 gradle.properties 中添加 hot.reload.enabled=true

答案:B

解析:从 Compose Multiplatform 1.10.0 开始,Compose Hot Reload 插件已捆绑并对所有包含桌面目标的项目默认启用。如果你的项目已经包含桌面目标,升级到 1.10.0 或更高版本即可开箱即用体验 Compose Hot Reload 功能。

第二组:统一 @Preview 注解(4题)

第5题 CMP 1.10.0 统一 @Preview 注解后,导入路径应为( )

A. org.jetbrains.compose.ui.tooling.preview.Preview

B. androidx.compose.ui.tooling.preview.Preview

C. androidx.compose.preview.Preview

D. compose.ui.preview.Preview

答案:B

解析:CMP 1.10.0 将预览统一在单一 @Preview 注解下,该注解可在 commonMain 源集中工作。所有其他注解已弃用,IDE 快速修复建议会帮助你将依赖项更新为 androidx.compose.ui.tooling.preview.Preview。

第6题 在 KMP 项目的 commonMain 中启用 Compose Preview 需要什么必要条件?( )

A. 只需要 iOS 目标

B. 需要一个 Android 目标,因为预览依赖 Android 库

C. 只需要 Desktop 目标

D. 不需要任何目标,预览在所有平台上原生可用

答案:B

解析:要在 Kotlin Multiplatform 项目的公共代码中启用 Compose 预览,你需要一个 Android 目标,因为预览依赖 Android 库。这是 @Preview 注解在 commonMain 中工作的前提条件。

第7题 以下哪个不是 @Preview 注解支持的参数?( )

A. widthDp

B. heightDp

C. locale

D. animationDuration

答案:D

解析:Compose Multiplatform 支持以下 @Preview 注解参数:name(显示名称)、group(分组名称)、widthDp(最大宽度)、heightDp(最大高度)、locale(语言环境)、showBackground(是否显示背景)、backgroundColor(背景颜色)。animationDuration 不是 @Preview 注解的参数。

第8题 @Preview 注解的 group 参数的作用是( )

A. 设置预览的分辨率

B. 支持对相关预览进行逻辑组织和选择性显示

C. 指定预览的语言环境

D. 控制预览的背景颜色

答案:B

解析:group 参数是预览的分组名称,支持对相关预览进行逻辑组织和选择性显示。例如,可以将同一组件的不同状态(暗色模式、不同语言等)归入同一分组,方便在 IDE 中管理大量预览。

第三组:MCP 服务器与 AI 集成(4题)

第9题 MCP 服务器在哪个 CMP 版本中引入?( )

A. CMP 1.10.0

B. CMP 1.11.0

C. CMP 1.12.0

D. CMP 1.13.0

答案:C

解析:CMP 1.12.0 为 Desktop 端 Compose Hot Reload 引入了实验性 MCP(Model Context Protocol)服务器,使 AI 编码代理能够与运行中的 Compose 应用实时交互。

第10题 以下哪项不是 MCP 服务器提供的工具?( )

A. take_screenshot

B. get_semantic_tree

C. type_text

D. compile_project

答案:D

解析:MCP 服务器提供的工具包括:status(获取应用状态)、take_screenshot(截取屏幕截图)、get_semantic_tree(检查语义树)、click(点击元素)、long_click(长按元素)、type_text(替换文本内容)、scroll(按像素滚动)、scroll_to_index(按项滚动)。compile_project 不是 MCP 服务器的工具,编译由 Gradle 构建系统处理。

第11题 MCP 服务器的启动方式是通过以下哪个 Gradle 任务?( )

A. hotRun

B. hotMcpServer

C. mcpStart

D. reloadServer

答案:B

解析:MCP 服务器通过 hotMcpServer Gradle 任务启动。它可以在没有运行中的应用时启动,会轮询等待应用、检测关闭并自动重连。在 .mcp.json 文件中配置时,args 数组中包含 "hotMcpServer"。

第12题 MCP 服务器对 AI 代理开发工作流的核心价值是( )

A. 自动生成 Compose 代码

B. 让代理能够验证自身编辑的结果,形成编辑-重载-验证的闭环

C. 替代 Gradle 构建系统

D. 自动部署应用到应用商店

答案:B

解析:MCP 服务器让 AI 代理能够触发重载、截图、检查语义树、模拟交互、读取日志,从而验证自身编辑的结果。代理可以确认重载是否成功、检查渲染后的 UI、捕获运行时异常,然后迭代------所有这些都无需人工描述屏幕上的内容。

第四组:语义树与调试工具(3题)

第13题 在 Compose 测试中打印语义树使用的函数是( )

A. printTree()

B. printToLog()

C. dumpSemantics()

D. logSemantics()

答案:B

解析 :在测试中,使用 composeTestRule.onRoot().printToLog("TAG") 输出语义树。printToLog() 函数将语义节点信息打印到日志中(debug 级别)。默认情况下会打印整个子层级,可以通过设置 maxDepth 参数来控制打印深度。

第14题 要查看未被合并的完整语义树,应该使用( )

A. composeTestRule.onRoot().printToLog("TAG")

B. composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")

C. composeTestRule.onAllNodes().printToLog("TAG")

D. composeTestRule.onRoot(maxDepth = 0).printToLog("TAG")

答案:B

解析 :默认情况下,语义树会合并具有 MergeDescendants 属性的节点。如果需要查看未被合并的完整语义树(例如在定位测试中的特定节点时),使用 composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")。

第15题 关于 Compose Hot Reload 的已知限制,以下说法正确的是( )

A. Hot Reload 不支持 Windows 平台

B. Hot Reload 在 Windows Dev Drive(ReFS)上不工作,因为 Gradle 文件系统监视不支持 ReFS

C. Hot Reload 可以在调试模式下正常使用断点

D. Hot Reload 支持在同一 IDE 实例中同时运行多个 Compose 应用

答案:B

解析:Compose Hot Reload 依赖 Gradle 文件系统监视来检测源文件更改,但 Gradle 的文件系统监视目前不支持 ReFS 文件系统。建议将项目移动到 NTFS 格式的驱动器。其他选项错误:A 选项 Hot Reload 支持 Windows;C 选项在调试模式下断点会被忽略;D 选项 Kotlin Multiplatform IDE 插件目前仅支持单个 Compose Hot Reload 连接,支持单个连接是刻意设计。

本课知识点总结

1. Compose Hot Reload 的核心能力

Compose Hot Reload 将代码更改直接应用到运行中的应用程序,与用于查看孤立组件的 Compose Preview 形成互补。自 CMP 1.10.0 起达到稳定状态并默认捆绑在 Gradle 插件中,需要 Kotlin 2.1.20+ 和与 Java 21 或更早版本兼容的 JVM 目标。运行方式包括 IDE 内运行配置 desktopApp [hot] 🔥 和命令行 ./gradlew :desktopApp:hotRun --auto。

2. 统一 @Preview 注解

CMP 1.10.0 将预览统一在单一 @Preview 注解下,导入路径为 androidx.compose.ui.tooling.preview.Preview,可在 commonMain 源集中工作。要在 commonMain 中启用预览,需要一个 Android 目标。支持参数包括 name、group、widthDp、heightDp、locale、showBackground、backgroundColor,允许在同一文件中预览组件的不同状态。Preview 与 Hot Reload 互补:前者用于组件级设计验证,后者用于应用级功能验证。

3. MCP 服务器与 AI 集成

CMP 1.12.0 引入实验性 MCP 服务器,通过 hotMcpServer Gradle 任务启动。提供 8 个工具:status、take_screenshot、get_semantic_tree、click、long_click、type_text、scroll、scroll_to_index。配置方式是在 .mcp.json 中声明。核心价值在于让 AI 代理形成编辑-重载-验证的闭环,代理可以确认热重载是否成功、检查渲染后的 UI、捕获运行时异常。

4. 语义树调试

语义树是 Compose UI 的结构化表示,每个节点携带语义信息。使用 printToLog() 在测试中输出语义树,使用 useUnmergedTree = true 查看未合并的完整树。语义树在定位测试失败原因、验证 UI 结构、AI 代理理解界面状态等场景中发挥关键作用。

5. IDE 工具链与已知限制

Kotlin Multiplatform IDE 插件提供 Compose 资源支持、commonMain 中的 @Preview、Hot Reload 集成、跨语言调试等功能。断点调试支持 Android、Desktop 和 Web(Wasm,需 IntelliJ IDEA 2025.3+ 和 JavaScript Debugger 插件)。Hot Reload 的已知限制包括:窗口闪烁(可通过 -Dcompose.reload.devToolsDetached=true 解决)、虚拟桌面兼容性问题、Windows Dev Drive(ReFS)不兼容、不支持调试模式断点、仅支持单个 Hot Reload 连接。ReFS 问题的推荐解决方案是将项目移至 NTFS 驱动器,同时将 .gradle 缓存保留在 Dev Drive 上以获得构建性能优势。


下一课预告:第4课将进入 UI 基础与核心组件模块,深入 Composable 函数与声明式 UI 思维,包括 @Composable 注解的编译机制、声明式与命令式的思维转变、Composable 函数的命名规范与设计原则,以及可组合项的组合与嵌套策略。

相关推荐
FungLeo1 天前
成为全栈·Flutter App 篇·内置 WebView:网页历史、App 返回与外链安全
flutter·网络安全·移动开发·webview·路由管理·成为全栈
传奇开心果编程1 天前
【Compose Multiplatform跨端声明式UI实用技术】第4课:Composable 函数与声明式 UI 思维
移动开发·编程语言理论·开发范式·编程框架·函数式思维·声明式ui·声明式ui思维
传奇开心果编程1 天前
【Compose Multiplatform跨端声明式UI实用技术】第1课:CMP 全景解读——为什么选择声明式跨平台UI
移动开发·编程语言理论·开发范式·编程框架·声明式ui·跨平台声明式ui·逻辑共享与ui共享
FungLeo2 天前
成为全栈·Flutter App 篇·分类、标签与搜索:移动端内容发现
flutter·移动开发·标签·搜索·分类管理·成为全栈
FungLeo2 天前
成为全栈·Flutter App 篇·列表分页与下拉刷新:去重、失败保留和返回位置
flutter·移动开发·下拉刷新·分页加载·列表优化·成为全栈
FungLeo5 天前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
传奇开心果编程6 天前
【React Native娓娓道来】第7课 表单与用户输入:TextInput、表单校验与键盘处理
移动开发·表单·编程语言理论·开发范式·编程框架·声明式ui·用户输入
传奇开心果编程7 天前
【React Native娓娓道来】第9课 动画与手势:Animated、Reanimated 与 Pressable 进阶
移动开发·动画·手势·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程7 天前
【声明式UI开发实用技术学与练】第2课 状态建模:把界面拆成可预测的数据
移动开发·编程语言理论·开发范式·编程框架·声明式ui