android-emulator について
Android エミュレーターで Flutter アプリを起動、確認、テストするための Bash ヘルパーです。開発者やエージェント向けに、`adb` と qemu コンソールを組み合わせ、アクセシビリティツリーの確認やマルチタッチ操作を行えます。
主な機能
- 01AVD の起動と終了
- 02Flutter アプリのビルドと実行
- 03アクセシビリティノードの確認
- 04ラベルまたは座標による操作
- 05スクリーンショットとログの取得
活用シーン
- 01エミュレーターでアプリの問題を再現
- 02Flutter アプリのスモークテスト
- 03画面ラベルを使ったナビゲーション
- 04画面状態や実行ログの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chunkytofustudios/ flutter-skills --skill android-emulatorインストール後、Claude Code で/android-emulatorを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
scripts/emu.shtests/dispatch.batshelp.batshelpers.bashparsers.batsui.batstests/fixtures/ui-dump.xmltests/stubs/adbconvertemulatorflutterfvmncpkillsipssleeptail詳細
Android エミュレーター(Flutter)
adb と qemu エミュレーターコンソールをラップする bash ヘルパーです。Android エミュレーター上で実行中の Flutter アプリを AI エージェントが確認(スクリーンショット、アクセシビリティツリー)し、操作(タップ、長押し、スワイプ、ピンチ)できるようにします。qemu には macOS アプリバンドルがないため、コンピューター操作 API ではエミュレーターウィンドウにアクセスできません。また、adb shell input はシングルタッチのみです。このスクリプトは、その両方の問題を解決します。
こんなときに使います
ユーザーが Android 上で Flutter アプリを起動、デバッグ、スモークテスト、QA、またはスクリーンショット撮影したい場合に使います。座標を推測したり、DevTools から手動でスクリーンショットを撮ったり、ユーザーに flutter run を自分で実行してもらったりする前に、このスキルを使ってください。
セットアップ
このスクリプトは、動作する Android SDK のインストール(adb と emulator が $PATH 上、または ~/Library/Android/sdk/ / ~/Android/Sdk/ にあること)、少なくとも 1 つの AVD の作成、および Flutter(または fvm)のインストールを前提とします。macOS 専用ユーティリティとして、sips(スクリーンショットのリサイズに使用。macOS に付属)を使います。Linux に移植する場合は、ImageMagick の convert に置き換えてください。
スクリプトは、このスキルを基準にした相対パス scripts/emu.sh にあります。スキルをプロジェクトに組み込んだら(README.md を参照)、エージェントはこの相対パスでスクリプトを呼び出します。
クイックスタート(コールドブートから操作可能な状態まで)
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 flutter を使います。run には必ず wait-run を組み合わせ、sleep は使わないでください。デーモンの stdin はなくなっているため、ビルド完了を判断する手掛かりはタイミングだけです。
コマンド
エミュレーターのライフサイクル
| コマンド | 動作 |
|---|---|
boot | AVD をコールドブート(-no-snapshot-load)で起動し、sys.boot_completed まで待機します。冪等です。デバイスがすでにオンラインなら、直ちに戻ります。AVD は emulator -list-avds が最初に表示するものがデフォルトです。ANDROID_EMU_AVD で固定できます。 |
exit | qemu コンソール(kill)経由で、実行中のエミュレーターを終了します。 |
health | 接続状態、AVD 名、解像度、フォアグラウンドアプリ、検出されたプロジェクトルート、検出されたパッケージ、Flutter ログのサイズを一度に表示します。状況を把握するため、まずこれを実行してください。デバイスが接続されていない場合は、0 以外の終了コードで終了します。 |
devices / size / foreground | adb devices / wm size / フォーカス中のアクティビティをそのまま表示します。 |
app-running | 対象パッケージがフォーカス中のアプリなら 0、そうでなければ 1 で終了します。スクリプト内で使用します。 |
アプリの制御
| コマンド | 動作 |
|---|---|
launch | インストール済みの APK を monkey で起動します(再ビルドはしません)。 |
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 | 1回タップします。 |
hold X Y [MS] | 長押しします(既定値は800ms)。 |
swipe X1 Y1 X2 Y2 [MS] | スワイプします(既定値は300ms)。msを短くするとフリックになります。 |
pinch out|in [CX CY SG EG] | 2本指でピンチします。out は拡大します。既定値:中心 (180,367)、開始間隔67、終了間隔333。ピンチ操作はqemuコンソール経由で実行します。adb input ではマルチタッチを実行できません。 |
入力 — ラベルベース(推奨 — 座標を推測する必要はありません)
これらのコマンドは、uiautomator dump で Android のアクセシビリティツリーを読み取り、指定した LABEL と text、content-desc、resource-id、または hint が一致するノードに対して操作します。完全一致が部分一致より優先されます。hint は空の TextField に対応します(Flutter の InputDecoration.labelText はここに表示されます)。
| コマンド | 説明 |
|---|---|
ui-list | 画面上にあるラベル付きノードの、人が読める形式の一覧を表示します。スクリーンショット上の中心座標、タップ/長押し/スクロールの可否、ラベルが含まれます。まずこれを実行して、操作可能な要素を確認してください。 |
ui-find LABEL | 最初に一致した要素のデバイスpx境界と、スクリーンショットpxでの中心座標を表示します。デバッグに役立ちます。 |
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)\])。
スクリーンショットと ui-list の使い分け
スクリーンショットは1枚あたり約30~60 KBで、会話のコンテキスト内ですぐに容量が増える一方、ui-list の出力は約2 KBです。通常は ui-list を使ってください。 画面上のラベルが変わっていれば、新しい画面に移動しています。これは多くの画面遷移で確認が必要なことです。
次の場合にのみ screenshot を使ってください。
- 内容が本質的に視覚的な場合 —
CustomPainter、画像やカメラのプレビュー、カラースウォッチ、サムネイルなど、ウィジェットではなくピクセルとして描画されるもの。 - ラベルでは状態を区別できない場合 — 同じ画面で、アクセシビリティツリーに反映されない状態変化がある場合(例:スライダーをドラッグして値を変更した場合や、同じテキストを再利用するチップを切り替えた場合)。
tap-labelの失敗をデバッグする場合 — ラベルが一致しない、または誤った要素をタップする場合は、画面の内容を把握するためにui-dumpを読むよりスクリーンショットを撮るほうが速いです。
目安:タップした後は、まず ui-list を確認してください。質問への答えが得られない場合にのみ、スクリーンショットを撮ってください。
ウィジェットを操作可能にする(Flutter Semantics)
uiautomator dump は Android のアクセシビリティツリーを読み取ります。Flutter アプリでは、アプリのセマンティクスツリーが公開されていない限り、そのツリーは空です。方法は 2 つあります。
-
デバッグビルドでセマンティクスを有効にする —
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 など)はセマンティクスを自動的に出力します。ナビゲーションバー、アプリバーのボタン、フォームフィールドなどを、ウィジェットごとにラップする必要はありません。
- 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を送信します。- ピンチ操作は
adb経由では実行できません。adb shell inputはシングルタッチのみです。sendevent /dev/input/event1によるマルチタッチには、root 権限(本番用 AVD ではadb rootが拒否されます)か、shellユーザーにはない SELinux 権限が必要です。pinchコマンドは qemu のホスト側コンソール(localhost:5554。認証トークンは~/.emulator_console_auth_tokenにあります)にフォールバックし、イベントを仮想入力デバイスに直接書き込みます。 - セマンティクスがない場合、アプリは不透明な矩形 1 つとして扱われます。 リリースビルド(または
ensureSemantics()を呼び出していないデバッグビルド)でui-listを実行しても、有用な情報は返されません。座標を推測するのではなく、アプリ側で修正してください。上のセクションを参照してください。 ui-listに表示されるのはラベル付きノードのみです。 ラベルのない親ノードやラッパーは表示されません。想定したノードが見つからない場合は、ui-dumpで生の XML を確認してください。- ホットリスタートとフルビルドの違い。
runは毎回、新しいflutter runを起動します。コード変更後にホットリロードしたい場合は、デーモンの stdin にr\nを送信します。ただし、このスクリプトはデーモンをバックグラウンドで実行するため(stdin は失われます)、代わりに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
ユーザーが見ている問題をデバッグする
scripts/emu.sh ui-list— 現在画面に表示されているものを確認します。- ユーザーの操作手順を再現します。各ステップで
tap-label "…"を実行します。 - 問題が疑われる操作の後、もう一度
ui-listを実行します。期待どおりに見える場合は、視覚的な不具合があるときだけスクリーンショットを撮ります。 scripts/emu.sh log 100— flutter ログの末尾を確認し、例外やアサーションを見つけます。
デバッグセッションを適切に停止する
scripts/emu.sh kill-run
エージェント内から TaskStop を実行しないでください。バックグラウンドタスクはすでに終了している可能性があり、いずれの場合も、強制停止するまではデバイス上でアプリが実行され続けます。
完全に終了する
scripts/emu.sh kill-run # stop flutter + app
scripts/emu.sh exit # shut down the emulator
スクリーンショットが幅360pxのJPEGである理由
PixelクラスのAVDでフル解像度のPNG(1080×2424)は600 KB~1.5 MBになり、会話のコンテキストを大きく圧迫します。PNGのリサイズだけではほとんど効果がありません。エンコーダーの圧縮が強くないためです。幅360pxで品質85のJPEGなら、UIラベルや小さな文字が読める状態を保ちながら、約25~60 KBになります。幅360pxの画像は、tap/hold/swipe/pinchの基準入力空間です。スクリプトがデバイス側のスケーリングを処理するため、呼び出し側から見た解像度は変わりません。ラベルが読みにくい場合(92を試してください)、またはファイルを小さくしたい場合(70を試してください)は、ANDROID_EMU_SHOT_QUALITYでJPEG品質を変更できます。
同時利用
呼び出しごとの一時パス(ANDROID_EMU_TMP_IDを付加。デフォルトは$$)に、スクリーンショット、UIダンプ、Flutterデーモンのログとpidfileを保存します。そのため、異なるエミュレーターデバイスを操作する2つのエージェントが互いのファイルを上書きすることはありません。run → wait-run → log → kill-runの一連の処理では、ANDROID_EMU_DEVICEとANDROID_EMU_TMP_IDを固定してください。screenshotは出力パスを表示するため、そのパスを読み取り、ハードコードしないでください。
1台のエミュレーターで実行できるFlutterデーモンは1つだけです。そのため、同じデバイスを使う2つのエージェントは調整する必要があります(通常、一方がrun/kill-runを担当し、両方が自由に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がある場合は、明示的に1つを指定してください。 |
flutter CLI | プロジェクトに.fvm/ディレクトリがあり、fvmがPATHにある場合はfvm flutterを使い、それ以外の場合はflutterを使います。ANDROID_EMU_FLUTTER_CMDで変更できます。 |
scripts/emu.sh healthを実行すると、解決された値が表示されます。問題があるように見える場合は、まずこれを実行してください。
環境変数による上書き
| 変数 | デフォルト | 用途 |
|---|---|---|
ANDROID_EMU_DEVICE | emulator-5554 | adbデバイスのシリアル番号 |
ANDROID_EMU_AVD | 一覧の最初の項目 | 起動するAVD |
ANDROID_EMU_PKG | gradleから解析 | applicationId |
ANDROID_EMU_PROJECT_ROOT | $PWDから上の階層をたどって検出 | Flutterプロジェクトのルート |
ANDROID_EMU_FLUTTER_CMD | flutterまたはfvm flutter | flutterコマンドの上書き |
ANDROID_EMU_CONSOLE_PORT | 5554 | qemuコンソールのポート |
ANDROID_EMU_SHOT_QUALITY | 85 | JPEG品質(1~100) |
ANDROID_EMU_WAIT_SECS | 180 | wait-runのタイムアウト |
ANDROID_EMU_BOOT_SECS | 180 | bootのタイムアウト |
ANDROID_EMU_TMP_ID | $$(スクリプトのPID) | 一時ファイルの接尾辞。同じエミュレーターを複数のエージェントで共有する場合は、呼び出し間で固定してください |