mcpField
A
android-emulator

android-emulator

github.com/chunkytofustudios测试与质量591 次安装26 周前更新
代码仓库

关于 android-emulator

用于 Flutter 开发者和 Agent 在 Android 模拟器中启动、检查和测试应用的 Bash 脚本。它结合 `adb` 与 qemu 控制台,支持读取无障碍界面树及多点触控操作。

主要功能

  1. 01启动和关闭 AVD
  2. 02构建并运行 Flutter 应用
  3. 03读取界面无障碍节点
  4. 04按标签或坐标执行触控
  5. 05截屏并查看应用日志

适用场景

  1. 01在模拟器中复现应用问题
  2. 02执行 Flutter 应用冒烟测试
  3. 03通过界面标签完成导航
  4. 04检查视觉状态或运行日志

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/chunkytofustudios/flutter-skills --skill android-emulator

安装后在 Claude Code 中输入/android-emulator确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

scripts/
emu.sh
tests/
dispatch.bats
help.bats
helpers.bash
parsers.bats
ui.bats
tests/fixtures/
ui-dump.xml
tests/stubs/
adb
convert
emulator
flutter
fvm
nc
pkill
sips
sleep
tail

详细介绍

Android 模拟器(Flutter)

一个 bash 辅助脚本,封装了 adb 和 qemu 模拟器控制台,让 AI Agent 能够查看(截图、无障碍树)并操作(点击、长按、滑动、捏合)在 Android 模拟器上运行的 Flutter 应用。电脑控制 API 无法访问模拟器窗口(qemu 没有 macOS 应用程序包),而 adb shell input 仅支持单点触控——此脚本弥补了这两个缺口。

适用场景

用户想要在 Android 上启动、调试、冒烟测试、QA 或截取 Flutter 应用的屏幕截图。请优先使用此技能,而不是猜测坐标、通过 DevTools 手动截图,或让用户自行运行 flutter run。

设置

脚本要求已安装可用的 Android SDK(adb 和 emulator 位于 $PATH 中,或安装在 ~/Library/Android/sdk/ / ~/Android/Sdk/ 下),至少创建了一个 AVD,并已安装 Flutter(或 fvm)。此工具仅适用于 macOS:sips(用于重新采样截图,是 macOS 自带工具)。在 Linux 上移植时,可改用 ImageMagick 中的 convert。

脚本位于此技能目录下的 scripts/emu.sh。将技能接入项目后(见 README.md),Agent 可通过该相对路径调用脚本。

快速开始(冷启动并进入交互模式)

scripts/emu.sh boot        # start an AVD (cold boot), idempotent
scripts/emu.sh run         # flutter build & install in background (~30–60s first time)
scripts/emu.sh wait-run    # blocks until attached or errors (180s timeout)
scripts/emu.sh health      # sanity check
scripts/emu.sh ui-list     # see what's on screen — start here, not screenshot

run 会自动检测项目根目录(向上查找 pubspec.yaml),如果项目指定了 fvm,则使用 fvm flutter。务必将 run 与 wait-run 搭配使用,而不是使用 sleep——守护进程的 stdin 已不可用,因此只能通过等待来判断构建是否完成。

命令

模拟器生命周期
命令功能
boot以冷启动模式启动 AVD(-no-snapshot-load),并阻塞等待 sys.boot_completed。支持幂等操作——如果设备已在线,则立即返回。默认使用 emulator -list-avds 列出的第一个 AVD;可通过 ANDROID_EMU_AVD 指定。
exit通过 qemu 控制台(kill)关闭正在运行的模拟器。
health一次性状态检查:连接状态、AVD 名称、分辨率、前台应用、检测到的项目根目录、检测到的包名、Flutter 日志大小。请先运行此命令以了解当前状态。如果设备未连接,则以非零状态码退出。
devices / size / foreground原始 adb devices / wm size / 当前焦点 Activity。
app-running如果目标包是当前焦点应用,则退出码为 0;否则为 1。可用于脚本。
应用控制
命令功能
launch使用 monkey 启动已安装的 APK(不重新构建)。
stop使用 am force-stop 停止应用。
run在后台启动 flutter run -d <device>。日志:/tmp/android-emu-flutter-<id>.log(每次调用单独记录,通过 ANDROID_EMU_TMP_ID 标识)。将守护进程 PID 写入 <log>.pid。如果存在 .fvm/,则使用 fvm flutter。
wait-run阻塞等待日志中出现 Flutter run key commands. 或构建失败。超时时间为 180 秒(ANDROID_EMU_WAIT_SECS)。务必将 run 与 wait-run 搭配使用,而不是使用 sleep。
kill-run终止 Flutter 守护进程(通过 <log>.pid;如果找不到,则使用 pkill -f flutter_tools.snapshot),并强制停止应用。
log [-f] [N]查看本次调用日志的最后 N 行(默认 50 行);使用 -f 可持续查看实时日志。可通过 grep 过滤,例如 log 500 | grep -E 'I/flutter.*\[(W|S)\]' 可筛选警告和严重日志(logcat 会在每个 print() 前加上 I/flutter ( PID): ,因此级别标记不会出现在行首)。
输入 — 基于坐标(所有坐标均为截图像素,空间宽度为 360)
命令功能
screenshot截取屏幕 → /tmp/android-emu-shot-<id>.jpg(宽 360px 的 JPEG,质量为 q85)。确切路径会打印到标准输出 — Read 该路径即可查看当前状态。每次调用使用不同路径,避免并发调用相互覆盖。
tap X Y单击。
hold X Y [MS]长按(默认 800ms)。
swipe X1 Y1 X2 Y2 [MS]滑动(默认 300ms)。ms 越短,滑动越快。
pinch out|in [CX CY SG EG]双指捏合。out 表示放大。默认值:中心点 (180,367),起始间距 67,结束间距 333。捏合操作通过 qemu 控制台执行 — adb input 无法进行多点触控。
输入 — 基于标签(优先使用 — 无需猜测坐标)

这些命令通过 uiautomator dump 读取 Android 的无障碍树,并对 text、content-desc、resource-id 或 hint 与给定 LABEL 匹配的节点执行操作。精确匹配优先于子串回退匹配。hint 可匹配空的 TextField(Flutter 的 InputDecoration.labelText 会显示在这里)。

命令功能
ui-list以人类可读的形式列出屏幕上带标签的节点:截图空间中心点、点击/长按/滚动标记及标签。从这里开始,以查找可操作的元素。
ui-find LABEL打印第一个匹配项的设备像素边界和截图像素中心点。用于调试。
ui-dump原始 uiautomator XML。当 ui-list 隐藏了你要找的节点时(例如未标记的父节点),很有用。
tap-label LABEL点击第一个匹配节点的中心。
hold-label LABEL [MS]长按第一个匹配节点的中心。

坐标使用截图空间。 ui-list 中的 cx cy 列与截图 JPEG 和 tap X Y 使用相同的 360 宽度坐标系,因此无需缩放即可相互对照。

ui-list 输出示例:

 cx   cy  flags          label
 27   70  tap            'Settings'
137  141  tap            'Get started'
 47  254  tap            'New project'
 45  760  tap            'Home\nTab 1 of 4'
135  760  tap            'Search\nTab 2 of 4'
225  760  tap            'Library\nTab 3 of 4'
315  760  tap            'Profile\nTab 4 of 4'

然后执行 tap-label "Search" — 子串匹配即可;无需包含 \nTab 2 of 4 后缀。

按严重级别筛选日志

如果应用使用带有 [F]/[I]/[W]/[S] 前缀的 package:logging,可以通过管道将 log 输出传给 grep。Logcat 会为每个 print() 添加 I/flutter ( PID): ,因此级别标签不会出现在行首 — 应在此前缀之后进行匹配:

scripts/emu.sh log 500 | grep -E 'I/flutter.*\[(W|S)\]'    # warnings + severe
scripts/emu.sh log -f  | grep --line-buffered -E 'I/flutter.*\[S\]'   # follow severe

根据应用输出的方案替换正则表达式(例如 \[(WARNING|SEVERE)\])。

选择 screenshot 还是 ui-list

每张截图约为 30–60 KB,在对话上下文中很快就会累积,而 ui-list 输出约为 2 KB。默认使用 ui-list。 如果屏幕上的标签发生变化,说明你已进入新屏幕 — 大多数导航步骤只需确认这一点。

仅在以下情况下使用 screenshot:

  1. 内容本质上是视觉内容 — CustomPainter、图像/相机预览、色板、缩略图等任何以像素而非组件形式渲染的内容。
  2. 标签无法区分状态 — 屏幕相同,但状态变化未反映在无障碍树中(例如,滑块被拖到新值,或切换的芯片复用了相同文本)。
  3. 调试 tap-label 失败 — 标签匹配失败或点击了错误的对象时,截图比阅读 ui-dump 更快地帮助你判断屏幕上显示了什么。

经验法则:点击后先检查 ui-list。只有在它无法回答问题时才截图。

让控件可寻址(Flutter Semantics)

uiautomator dump 会读取 Android 的无障碍树。在 Flutter 应用中,除非公开应用的语义树,否则该树为空。有两种方式:

  1. 在调试构建中启用语义 — 在 main() 靠前的位置添加:

    import 'package:flutter/foundation.dart';
    import 'package:flutter/semantics.dart';
    
    void main() {
      if (kDebugMode) {
        SemanticsBinding.instance.ensureSemantics();
      }
      runApp(const MyApp());
    }
    

    Material 控件(BottomNavigationBar、带 tooltip: 的 IconButton、TextButton、TextField 等)会自动生成 Semantics,因此导航栏、应用栏按钮、表单字段等无需逐个控件进行包装。

  2. 在 AVD 上启用无障碍服务 — TalkBack 或类似服务会强制平台生成该树。此方法较为繁琐;方式 (1) 更简洁。

如果 ui-list 输出 (no labelled nodes found — is Flutter semantics enabled?),说明两种方式都未生效。

对于需要可寻址的纯图标自定义控件:

Semantics(
  identifier: 'some_stable_id',   // optional, locale-invariant
  label: 'Human readable name',
  child: ...,
)

测试时首选 identifier,因为它不会随语言区域变化;label 则是辅助技术(以及 tap-label)读取的内容。

注意事项

  • adb shell input swipe X Y X Y MS 不是长按。 它发送 DOWN/UP 的间隔太短,Flutter 的 LongPressGestureRecognizer 会将其视为已取消的点击,因此不会触发 onLongPress。请使用 hold(或 hold-label)——它们会发送一对真实的 motionevent DOWN、等待和 motionevent UP 事件。
  • Pinch 不会通过 adb 执行。 adb shell input 仅支持单点触控。通过 sendevent /dev/input/event1 实现多点触控需要 root(生产环境的 AVD 会拒绝 adb root)或 shell 用户不具备的 SELinux 权限。pinch 命令会退回使用 qemu 的主机端控制台(localhost:5554,认证令牌位于 ~/.emulator_console_auth_token),直接向虚拟输入设备写入事件。
  • 没有语义时,整个应用就是一个不透明的矩形。 在发布构建中(或未调用 ensureSemantics() 的调试构建中)执行 ui-list,不会返回任何有用内容。应在应用中修复,而不是猜坐标 — 参见上文。
  • ui-list 只显示带标签的节点。 没有标签的父节点或包装器不会出现;如果预期的节点缺失,请使用 ui-dump 查看原始 XML。
  • 热重启与完整重建。 run 总会启动一个新的 flutter run。如果想在代码更改后热重载,可向守护进程的标准输入发送 r\n — 但脚本会将其置于后台(因此标准输入不可用),并假设你会再次执行 kill-run 和 run。如需更紧凑的开发循环,请在自己的终端中运行 flutter run,并在此处仅使用输入和截图命令。
  • 首次启动对话框。 许多应用首次启动时会显示启动页、引导页或升级提示。使用 ui-list 查看阻碍操作的内容,然后使用 tap-label 将其关闭(例如 tap-label "Close"、tap-label "Skip")。不要依赖固定的等待时间 — 等到 ui-list 显示预期界面。

常见工作流

从零开始冷启动
scripts/emu.sh boot
scripts/emu.sh run
scripts/emu.sh wait-run
scripts/emu.sh health
scripts/emu.sh ui-list
调试用户正在遇到的问题
  1. scripts/emu.sh ui-list — 当前屏幕上是什么?
  2. 重现用户的操作路径:每一步都使用 tap-label "…"。
  3. 执行可疑操作后,再次运行 ui-list。如果界面看起来正常,只有在问题与视觉效果有关时才截图。
  4. scripts/emu.sh log 100 — 查看 Flutter 日志末尾,捕捉异常或断言错误。
正常结束调试会话
scripts/emu.sh kill-run

不要尝试在 Agent 中执行 TaskStop — 后台任务可能已经被回收;无论如何,在强制停止之前,应用都会继续在设备上运行。

完全清理
scripts/emu.sh kill-run    # stop flutter + app
scripts/emu.sh exit        # shut down the emulator

为什么截图是宽 360px 的 JPEG

全分辨率 PNG(Pixel 级 AVD 上为 1080×2424)大小为 600 KB–1.5 MB,会大幅增加对话上下文。仅缩小 PNG 几乎没有帮助——编码器压缩力度不够。宽 360px、质量为 q85 的 JPEG 大约为 25–60 KB,同时 UI 标签和小字仍清晰可辨。宽 360px 的图像是 tap/hold/swipe/pinch 的标准输入空间——脚本会自行处理设备端缩放,因此从调用方的角度来看,分辨率保持不变。如果标签难以辨认(可尝试 92)或需要更小的文件(可尝试 70),请通过 ANDROID_EMU_SHOT_QUALITY 覆盖 JPEG 质量。

并发使用

每次调用使用独立的临时路径(后缀为 ANDROID_EMU_TMP_ID,默认值为 $$)来存放截图、UI 转储以及 flutter 守护进程日志和 pidfile,因此两个操作不同模拟器设备的 Agent 不会互相覆盖文件。在整个 run → wait-run → log → kill-run 调用链中固定 ANDROID_EMU_DEVICE 和 ANDROID_EMU_TMP_ID。screenshot 会输出其结果路径——请读取该路径,不要将其硬编码。

一台模拟器仍然只能运行一个 flutter 守护进程,因此在同一设备上操作的两个 Agent 必须协调(通常由一个 Agent 负责 run/kill-run,两个 Agent 都可以自由使用 log/screenshot/ui-list)。共享状态包括:模拟器输入流(同时进行的点击会交错)和设备尺寸缓存。

自动检测

项目方式
项目根目录从 $PWD 向上遍历,查找 pubspec.yaml。使用 ANDROID_EMU_PROJECT_ROOT 覆盖。
applicationId从 android/app/build.gradle 或 android/app/build.gradle.kts 中解析(第一行 applicationId "…")。使用 ANDROID_EMU_PKG 覆盖。
AVD如果未设置 ANDROID_EMU_AVD,则使用 emulator -list-avds 的第一项。有多个 AVD 时,请明确固定一个。
flutter CLI如果项目有 .fvm/ 目录且 PATH 中包含 fvm,则使用 fvm flutter;否则使用 flutter。使用 ANDROID_EMU_FLUTTER_CMD 覆盖。

scripts/emu.sh health 会打印解析出的值——如果有任何异常,请先运行它。

环境变量覆盖

变量默认值用途
ANDROID_EMU_DEVICEemulator-5554adb 设备序列号
ANDROID_EMU_AVD第一项要启动的 AVD
ANDROID_EMU_PKG从 gradle 解析applicationId
ANDROID_EMU_PROJECT_ROOT从 $PWD 向上遍历Flutter 项目根目录
ANDROID_EMU_FLUTTER_CMDflutter 或 fvm flutterflutter 命令覆盖值
ANDROID_EMU_CONSOLE_PORT5554qemu 控制台端口
ANDROID_EMU_SHOT_QUALITY85JPEG 质量(1–100)
ANDROID_EMU_WAIT_SECS180wait-run 超时
ANDROID_EMU_BOOT_SECS180boot 超时
ANDROID_EMU_TMP_ID$$(脚本 PID)临时文件后缀;多个 Agent 共用一台模拟器时,请在多次调用间固定该值

评论

支持 Markdown