最初に崩れたのは、ある壁紙アプリの設定画面でした。AI Studio に「設定項目をカード型に並べ替えて」と頼み、埋め込みエミュレータで確認したときは、余白も角丸も思ったとおりでした。安心して USB でつないだ実機に送り、そこで初めて、一番下のカードがナビゲーションバーの下に半分隠れているのに気づきました。
エミュレータでは緑、実機で赤。この組み合わせがいちばん厄介です。生成も実行も実機転送も一画面でつながったいま、確認の往復は驚くほど速くなりました。けれど速くなったのは「エミュレータでの確認」であって、「実機での確認」ではありません。私自身、最初の数週間はこの差を甘く見ていて、内部テストに配ってからテスターに指摘される、という遠回りを何度かしました。
この記事は、AI Studio や Antigravity が生成した Compose アプリに対して、エミュレータと実機の差を先回りで塞ぐ検証ゲートをどう据えるか、という話です。生成が速くなったぶん、検証だけは意図して遅く、固く保つ。その線引きと実装をまとめます。
なぜエミュレータは通って実機で崩れるのか
実機差は気まぐれに見えて、出どころはおおむね四つに整理できます。先に地図を持っておくと、生成物のどこを疑えばよいかが定まります。
システムインセット — ノッチとナビゲーションバー
ひとつめはシステムインセット です。エミュレータは素のジェスチャーナビゲーションや切り欠きのない画面で動くことが多く、生成されたレイアウトが WindowInsets を無視していても表面化しません。実機にはノッチ、パンチホール、3 ボタンナビ、丸い角があり、ここで初めて要素が隠れます。冒頭のカードが沈んだのも、生成コードが navigationBarsPadding() を当てていなかったためでした。
フォントスケール — 固定 dp の弱点
ふたつめはフォントとスケール です。実機のユーザーは表示サイズやフォントスケールを変えています。エミュレータの初期値(スケール 1.0)でだけ整って見える固定 dp の高さは、fontScale 1.3 の実機でテキストがクリップされます。AI が生成するレイアウトは、見た目を整えるために固定高さを置きがちで、ここが弱点になりやすいところです。
GPU レンダリング — エフェクトの落ち方
みっつめはGPU レンダリングの差 です。エミュレータはホストの GPU を使うため、blur、graphicsLayer、RenderEffect のようなエフェクトが滑らかに出ます。実機の特定 GPU・特定 OS では同じエフェクトが落ちたり、目に見えるカクつきになったりします。壁紙アプリのようにぼかしを多用すると、この差は無視できません。
ロケールと RTL — 反転で崩れる
よっつめはロケールと右書き です。エミュレータを日本語のまま確認していると、アラビア語などの RTL で要素が反転して崩れる箇所に気づけません。日本語圏向けでも、ストアの審査や海外テスターで RTL は普通に踏みます。
この四系統を頭に置くと、「生成物を実機で確認する」が「四つの観点で実機差を狙い撃ちする」に変わります。漠然と眺めるより、はるかに早く崩れを見つけられます。
観点を、消えないチェックに変える
頭の中の観点は、疲れている夜には抜けます。私は四系統を、消えないチェックとしてコードとプレビューに固定することにしました。生成のたびに人が思い出す前提を捨てる、というのが狙いです。
まず、インセットとフォントスケールは Compose プレビューで先に炙り出します。生成されたコンポーザブルに対し、極端な条件のプレビューを並べておくと、エミュレータを起動する前に崩れが目に入ります。
// 実機差を炙り出す「いじわるプレビュー」を生成物の隣に固定する
// fontScale を上げ、狭い高さに押し込み、RTL を当てる
@Preview (
name = "Large font + short height" ,
fontScale = 1.5f ,
heightDp = 360 ,
showBackground = true ,
)
@Preview (
name = "RTL locale" ,
locale = "ar" ,
showBackground = true ,
)
@Composable
private fun SettingsCardStressPreview () {
AppTheme {
// 実機のシステムバー領域を擬似的に差し込み、インセット忘れを可視化する
Box (Modifier. windowInsetsPadding (WindowInsets.systemBars)) {
SettingsCardList (items = sampleSettingsItems)
}
}
}
fontScale = 1.5f と heightDp = 360 を当てるだけで、固定 dp 高さに押し込まれたテキストはプレビュー上でクリップされて見えます。locale = "ar" を足せば、左右非対称なパディングがそのまま反転して崩れる箇所が出ます。生成直後に AI へ「このプレビューで崩れているので fontScale に追従するレイアウトへ直して」と返せば、エミュレータを開く前に一周回せます。
インセットの取りこぼしは、レイアウト側で恒久的に塞ぎます。スクロールする一覧なら、コンテンツの最後がナビゲーションバーに隠れないよう、インセットをパディングへ変換しておきます。
// 一覧の末尾がシステムバーに沈まないよう、インセットを contentPadding に流し込む
LazyColumn (
contentPadding = WindowInsets.systemBars
. add ( WindowInsets (top = 8 .dp, bottom = 8 .dp))
. asPaddingValues (),
) {
items (settingsItems, key = { it.id }) { item ->
SettingsCard (item)
}
}
これで冒頭のカードが沈む事故は構造的に起きなくなります。大切なのは、生成物に対してこの修正を「毎回お願いする」のではなく、テンプレート側に持たせて生成の出発点に組み込むことです。AI に任せる範囲を、崩れにくい土台の上に限定するわけです。本番運用に入る前にこの土台を固めておくことを強くお勧めします。落とし穴は、生成のたびに人が同じ注意点を思い出せると過信することにあります。
Android CLI で、実機差の検証を自動の門にする
2026 年に入って Android CLI が 1.0 安定版になり、IDE を開かずにセマンティック解析・Compose プレビューのレンダリング・UI テストの実行ができるようになりました。私はこれを、生成と人の目のあいだに置く「自動の門」として使っています。プレビューのスナップショットを撮り、前回との差分を見るだけでも、四系統の崩れの多くは機械が先に見つけます。
考え方はこうです。先ほどのいじわるプレビューを、CI 上でビットマップに焼き、基準画像と比較します。差分がしきい値を超えたら、その生成は人のレビュー待ちに落とす。エミュレータの「緑」を信用せず、固定した条件のレンダリング差だけを信用します。
#!/usr/bin/env bash
# preview-parity-gate.sh — Compose プレビューを焼いて基準画像と比較する門
set -euo pipefail
OUT_DIR = "build/preview-shots"
BASELINE_DIR = "screenshots/baseline"
THRESHOLD = "${ PARITY_THRESHOLD :- 0 . 5 }" # 差分ピクセル率の許容上限(%)
# Android CLI でプレビューをレンダリングし、PNG として書き出す
android-cli compose render \
--module app \
--previews "com.example.settings.SettingsCardStressPreview" \
--output " $OUT_DIR "
fail = 0
for shot in " $OUT_DIR "/*.png ; do
name = "$( basename " $shot ")"
base = " $BASELINE_DIR / $name "
if [ ! -f " $base " ]; then
echo "⚠️ 基準画像なし: $name (初回は人が確認して baseline に登録)"
fail = 1
continue
fi
# ImageMagick の AE 指標で異なるピクセル数を取り、率に換算
diff_px = $( compare -metric AE " $base " " $shot " null: 2>&1 || true )
total_px = $( identify -format "%[fx:w*h]" " $base " )
ratio = $( awk -v d=" $diff_px " -v t=" $total_px " 'BEGIN { printf "%.3f", (d/t)*100 }' )
echo " $name : 差分 ${ ratio }%"
awk -v r=" $ratio " -v th=" $THRESHOLD " 'BEGIN { exit (r > th) ? 1 : 0 }' || {
echo "❌ しきい値超過: $name (${ ratio }% > ${ THRESHOLD }%)→ 人のレビューへ"
fail = 1
}
done
exit $fail
このスクリプトを生成後の最初の関門に置くと、AI が画面を作り直すたびに「意図しない見た目の変化」が自動で浮かびます。意図した変更なら基準画像を更新して通し、意図しない崩れならその場で止める。判断の入口を人からスクリプトへ移すことで、夜間の自動生成でも見た目の回帰を取りこぼさなくなります。
GPU 依存のエフェクトは、プレビューだけでは捕まえきれません。ここは実機での UI テストに任せます。Android CLI から計測系のテストを走らせ、ぼかしを多用する画面のフレーム時間に上限を設けておきます。
// ぼかしを多用する画面が、実機で目に見えてカクつかないかを門にする
@Test
fun blurredWallpaperGrid_staysWithinFrameBudget () {
benchmarkRule. measureRepeated (
packageName = "com.example.wallpaper" ,
metrics = listOf ( FrameTimingMetric ()),
iterations = 5 ,
setupBlock = { startActivityAndWait () },
) {
device. findObject (By. res ( "wallpaper_grid" )). fling (Direction.DOWN)
}
// frameDurationCpuMs の P95 を実測の基準と照らす。
// 生成で blur を増やした結果カクついたら、ここで赤になる。
}
エフェクトの軽さは見た目では判断できないので、数字で門を作ります。生成によってぼかしが増え、P95 のフレーム時間が基準を超えたら、機械が先に気づいてくれます。
直させてよい失敗と、人が決める失敗を分ける
門を作ると、次に決めたくなるのは「赤になったとき、AI に直させてよいか」です。私はここを契約として書き出しました。曖昧なまま自動運用に載せると、AI が見た目を取り繕って数字だけ通す、という最悪の回避が起きるからです。
直させてよいのは、原因が構造的にはっきりしている失敗です。インセット忘れ、固定高さ、fontScale 非追従、RTL のパディング反転。これらは「正しい直し方」が決まっているので、差分とともに AI へ差し戻し、再生成させて門を通します。
人が決めるのは、トレードオフを含む失敗です。GPU 依存のエフェクトでフレーム時間が超えたとき、ぼかしを諦めるのか、解像度を落とすのか、対象端末を絞るのか。これは作品としての見え方に関わるので、私自身が判断します。アプリの表情をどこまで守るかは、自動化に明け渡したくない最後の領域です。
門で赤になった原因 扱い 誰が決めるか
インセット忘れ・固定高さ・RTL 反転 差分を添えて AI に再生成 機械が差し戻し、人は結果だけ確認
fontScale 非追従でクリップ 追従レイアウトへ AI が修正 機械が差し戻し
GPU エフェクトでフレーム超過 表現と性能の天秤 人が判断
基準画像との意図的な差分 baseline 更新で通す 人が承認
この切り分けを持っておくと、自動生成の速さを活かしながら、見た目の最後の責任だけは手元に残せます。速いのは生成、固いのは検証、譲らないのは表情。三つの速度を別々に保つ、というのが私の落としどころでした。
どの実機で確かめるか、を絞る
実機差を塞ぐといっても、手元のすべての端末で毎回確かめるのは現実的ではありません。私は「自分のユーザーが実際に使っている端末」へ寄せて、検証する実機を意図的に絞っています。
判断材料はストアの統計です。Google Play のアプリ内のデバイス分布を見れば、どの画面サイズ・どの OS バージョン・どのメーカーが多いかがわかります。私の壁紙アプリでは、上位の数機種で利用の大半を占めていたので、検証はその数機種に集中させました。網羅より、当たる確率の高いところを厚く見るほうが、一人で回す運用には合っています。
四系統の観点と合わせると、選ぶべき実機の像が定まります。インセットの差を見るなら切り欠きのある機種を一台、フォントスケールは表示サイズを上げた一台、GPU はぼかしが重くなりやすい廉価帯を一台。意図して偏らせた数台のほうが、新品のハイエンド一台で眺めるより、ずっと多くの崩れを拾ってくれます。
速さを足場に、固さを先に置く
AI Studio が一画面でつないでくれたのは、可逆な作業の往復コストでした。その速さは本物で、私の制作のテンポを確かに変えました。だからこそ、速くなった工程の先に、意図して固い門を一つ置いておく。エミュレータの緑を最終判定にせず、四系統の実機差をプレビューと実機テストで先回りする。
次に試すなら、いまいちばん崩れた記憶のある一画面に、いじわるプレビューを一組だけ足してみてください。fontScale と狭い高さと RTL を当てるだけで、エミュレータでは見えなかった弱点が、生成を回す前に目に入ってきます。そこから門を一段ずつ固めていけば、生成の速さと作品の表情は、両立できると感じています。