Slide 1

Slide 1 text

UI仕様を「見えるもの」にする Compose Screenshot Testing とギャラリーで支 える、AIエージェント時代の Android UI開発 Taichi Sato / @syarihu Giftmall, Inc. Android Engineer 1

Slide 2

Slide 2 text

Agenda 01 AIエージェント時代の並列実装と 確認コストの問題 02 Compose Screenshot Testing の概要 03 Compose Screenshot Testingを運用して 見えてきた課題とその解決策 04 生成したスクリーンショットの活用 05 動作確認と、AIに任せるための土台 2

Slide 3

Slide 3 text

AIエージェント時代の並列実装と 確認コストの問題 3

Slide 4

Slide 4 text

複数のタスクを同時に進めるようになった 4

Slide 5

Slide 5 text

タスクは細かく分解する ● 1つのタスクを細分化して AIが並列で実装しやすくする ● UI実装を伴うタスクでは、 親 Issue #100 商品検索画面の実装 サブ Issue #101 TopAppBar サブ Issue #102 絞り込み フィルター サブ Issue #103 価格帯タブ UIコンポーネント単位で Issueを起票 5

Slide 6

Slide 6 text

git worktreeで並列に実装する /taskhub https://github.com/syarihu/workbench/issues/101 AIエージェントへのタスク依頼からPR作成までのフロー AIによる自動実装 セルフレビュー PR自動作成 git worktreeを活用して 並列に自動実装 サブエージェントで AI実装を セルフレビュー 実装・レビューが終わったら自 動でPRを作成 6

Slide 7

Slide 7 text

UI確認はAndroid Studioでworktreeを開く  Android Studioで worktreeを開く  インデックス 作成を待つ   ビルドを待つ アプリを実行して  画面を確認する これが、並行稼働する PRの数だけ毎回発生する 7

Slide 8

Slide 8 text

git worktreeを 毎回ASで開くのめんどくさい ... 8

Slide 9

Slide 9 text

全部PR上で確認できないか? 9

Slide 10

Slide 10 text

Previewからスクリーンショットが作れれば … ● UIコンポーネントの実装はPR上で確認できる ● 現在のUIのスクリーンショットがリポジトリにあれば… ○ Before / Afterもスクショで確認できる ○ CIでコードの変更による意図しないレイアウト崩れに 気づきやすくなる 10

Slide 11

Slide 11 text

Compose Screenshot Testing 11

Slide 12

Slide 12 text

Compose Screenshot Testingとは ● Google公式のスクリーンショットテストプラグイン  libs.versions.toml [plugins] screenshot = { id = "com.android.compose.screenshot", version.ref = "0.0.1-alpha16" } 12

Slide 13

Slide 13 text

Compose Screenshot Testingとは ● Android Studioのプレビューと同じLayoutLibで、 @Previewを付けたComposableをエミュレータや実機を 使わずJVM上でレンダリングする 13

Slide 14

Slide 14 text

Compose Screenshot Testingとは  ProductItem.kt ● Android Studioのプレビューと同じLayoutLibで、 @VisibleForTesting @Preview @Previewを付けたComposableをエミュレータや実機を @Composable internal fun ProductItemPreview( 使わずJVM上でレンダリングする @PreviewParameter(ProductParamProvider.:class) product: Product ) { ProductItem(product = product) } 14

Slide 15

Slide 15 text

Compose Screenshot Testingとは  ProductItem.kt ● Android Studioのプレビューと同じLayoutLibで、  ProductItemScreenshotTest.kt @VisibleForTesting @Preview @Previewを付けたComposableをエミュレータや実機を internal class ProductItemScreenshotTest { @Composable @PreviewTest internal fun ProductItemPreview( 使わずJVM上でレンダリングする @Preview( @PreviewParameter(ProductParamProvider.:class) product: Product name = "ProductItem", ) { locale = "ja", ProductItem(product = product) uiMode = Configuration.UI_MODE_NIGHT_NO, } ) @Composable fun ProductItemTest() { ProductItemPreview(product = ProductParamProvider().values.first()) } } 15

Slide 16

Slide 16 text

Compose Screenshot Testingとは ● updateタスクを実行すると、描画結果が参照画像として 保存される Terminal $ ./gradlew updateDebugScreenshotTest BUILD SUCCESSFUL in 1s $ tree src/screenshotTestDebug/reference/ src/screenshotTestDebug/reference/ └── ProductItemScreenshotTest └── ProductItemTest_ProductItem_243dbe2f_0.png 16

Slide 17

Slide 17 text

Compose Screenshot Testingとは ● updateタスクを実行すると、描画結果が参照画像として 保存される Terminal $ ./gradlew updateDebugScreenshotTest BUILD SUCCESSFUL in 1s $ tree src/screenshotTestDebug/reference/ src/screenshotTestDebug/reference/ └── ProductItemScreenshotTest └── ProductItemTest_ProductItem_243dbe2f_0.png 17

Slide 18

Slide 18 text

Compose Screenshot Testingとは ● validateタスクを実行すると、最新の描画結果と参照画 像を比較する Terminal $ ./gradlew validateDebugScreenshotTest > Task :validateDebugScreenshotTest FAILED There were failing tests. See the report at: build/reports/screenshotTest/preview/debug/index.html 18

Slide 19

Slide 19 text

Compose Screenshot Testingとは ● 差分があればテストが失敗し、reference・actual・diff の画像を並べたHTMLレポートが出力される 19

Slide 20

Slide 20 text

Compose Screenshot Testingとは ● 差分があればテストが失敗し、reference・actual・diff の画像を並べたHTMLレポートが出力される 20

Slide 21

Slide 21 text

なぜCompose Screenshot Testingを選んだのか ● Android StudioのPreviewと同じLayoutLibで撮れる ○ @Previewの指定もそのまま反映される 21

Slide 22

Slide 22 text

なぜCompose Screenshot Testingを選んだのか ● 参照画像が既定でソースツリーにコミットされる ○ PR上のpermalink運用がそのまま組める ● 足す依存と設定が少ない ○ プラグイン、有効化フラグ、テスト用依存2つ 22

Slide 23

Slide 23 text

Compose Screenshot Testingを導入する ● プラグインの適用・フラグ・依存の追加はconvention pluginにまとめる 23

Slide 24

Slide 24 text

Compose Testingを導入する ScreenshotTestConventionPlugin.kt  Screenshot class ScreenshotTestConventionPlugin : Plugin { override fun apply(target: Project) = with(target) { pluginManager.apply("com.android.compose.screenshot") extensions.configure { @Suppress("UnstableApiUsage") experimentalProperties["android.experimental.enableScreenshotTest"] = true } val libs = extensions.getByType().named("libs") dependencies { add("screenshotTestImplementation", libs.findLibrary("jetpackComposeTooling").get()) add("screenshotTestImplementation", libs.findLibrary("screenshotValidationApi").get()) } } } 24

Slide 25

Slide 25 text

Compose Screenshot Testingを導入する ● プラグインの適用・フラグ・依存の追加はconvention pluginにまとめる ● 各モジュールは1行追加するだけで導入できる 25

Slide 26

Slide 26 text

Compose Screenshot Testingを導入する ● プラグインの適用・フラグ・依存の追加はconvention pluginにまとめる ● 各モジュールは1行追加するだけで導入できる  feature/product/build.gradle.kts plugins { id("com.example.android.feature") id("com.example.android.library.compose") id("com.example.android.library.screenshotTest") } 26

Slide 27

Slide 27 text

Compose Screenshot Testingのテストの書き方 ● テストクラスはmainと同じパッケージのscreenshotTestソー スセットに置く 27

Slide 28

Slide 28 text

Compose Screenshot Testingのテストの書き方  ProductItemScreenshotTest.kt internal class ProductItemScreenshotTest { ● テストクラスはmainと同じパッケージのscreenshotTestソー @PreviewTest @Preview( スセットに置く ▼ src ▶ main ▼ screenshotTest ▼ kotlin name = "ProductItem", locale = "ja", uiMode = Configuration.UI_MODE_NIGHT_NO, ▼ com.example.app ▼ compose ▼ product ProductItemScreenshotTest.kt ) @Composable fun ProductItemTest() { ProductItemPreview( product = ProductParamProvider().values.first() ) } } 28

Slide 29

Slide 29 text

Compose Screenshot Testingのテストの書き方 ● PreviewはLight/Darkのペアで撮り、Dark側は名前 の末尾にDarkを付ける ○ Preview名は参照画像のファイル名に使われる ● 名前だけでLight/Darkを区別できると、人にもAIに も分かりやすい ● PRに貼る差分表やギャラリーなど、画像を名前で扱 う場面でそのまま効いてくる 29

Slide 30

Slide 30 text

 ProductItemScreenshotTest.kt Compose Screenshot Testingのテストの書き方 internal class ProductItemScreenshotTest { @PreviewTest @Preview( name = "ProductItem", locale = "ja", uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Preview( name = "ProductItemDark", locale = "ja", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, backgroundColor = 0xC9000000, ) @Composable fun ProductItemTest() { ProductItemPreview(product = ProductParamProvider().values.first()) } ● PreviewはLight/Darkのペアで撮り、Dark側は名前の末尾 にDarkを付ける ● Preview名は参照画像のファイル名に使われる ● 名前だけでLight/Darkを区別できると、人にもAIにも分かり やすい ● PRに貼る差分表やギャラリーなど、画像を名前で扱う場面で そのまま効いてくる } 30

Slide 31

Slide 31 text

 ProductItemScreenshotTest.kt Compose Screenshot Testingのテストの書き方 internal class ProductItemScreenshotTest { ☀ 生成されるファイル名 (Light) @PreviewTest @Preview( ProductItemTest_ProductItem_243dbe2f_0.png name = "ProductItem", locale = "ja", uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Preview( name = "ProductItemDark", locale = "ja", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, backgroundColor = 0xC9000000, 🌙 生成されるファイル名 (Dark) ) @Composable ProductItemTest_ProductItemDark_4a47b0e9_0.png fun ProductItemTest() { ProductItemPreview(product = ProductParamProvider().values.first()) } ● PreviewはLight/Darkのペアで撮り、Dark側は名前の末尾 にDarkを付ける ● Preview名は参照画像のファイル名に使われる ● 名前だけでLight/Darkを区別できると、人にもAIにも分かり やすい ● PRに貼る差分表やギャラリーなど、画像を名前で扱う場面で そのまま効いてくる } 31

Slide 32

Slide 32 text

Compose Screenshot Testingを運用して 見えてきた課題とその解決策 32

Slide 33

Slide 33 text

Compose Screenshot Testingを運用して 見えてきた課題 01 手元のMacとCIで生成されるスクショに差分が出る 02 スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい 03 PR上で画像の差分が見えないことがある 04 ネットワーク画像が撮れない 05 Preview関数やテストクラスの命名がばらばらになる 33

Slide 34

Slide 34 text

Compose Screenshot Testingを運用して 見えてきた課題 01 手元のMacとCIで生成されるスクショに差分が出る 02 スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい 03 PR上で画像の差分が見えないことがある 04 ネットワーク画像が撮れない 05 Preview関数やテストクラスの命名がばらばらになる 34

Slide 35

Slide 35 text

手元のMacとCIで生成されるスクショに差分が出る ● コードは何も変えていないのに、CIのvalidateだけが 失敗する ● Macで生成した画像と、CI(Ubuntu)で生成した画像 が一致しない 35

Slide 36

Slide 36 text

手元のMacとCIで生成されるスクショに差分が出る ● コードは何も変えていないのに、CIのvalidateだけが 失敗する ● Macで生成した画像と、CI(Ubuntu)で生成した画像 が一致しない 36

Slide 37

Slide 37 text

手元のMacとCIで生成されるスクショに差分が出る ● コードは何も変えていないのに、CIのvalidateだけが 失敗する ● Macで生成した画像と、CI(Ubuntu)で生成した画像 が一致しない 37

Slide 38

Slide 38 text

手元のMacとCIで生成されるスクショに差分が出る ● コードは何も変えていないのに、CIのvalidateだけが 失敗する ● Macで生成した画像と、CI(Ubuntu)で生成した画像 が一致しない 38

Slide 39

Slide 39 text

差分は、目では見つけられない ±1 ● 差分が出たのは、1モジュール114枚のうち9枚だ け ● どれも文字やイラストの縁の±1 ● たった3ピクセルでも、テストは失敗する 39

Slide 40

Slide 40 text

layoutlibのネイティブ実 装はプラットフォームごと に違う ● 描画はlayoutlibのネイティブコード で行われ、Mac版(mac-arm64)と Linux版(x86_64)で丸めの結果が わずかに違う ● Composeのコードで制御できる層 ではないので、アプリ側では治せな い Composeコード layoutlib (mac-arm64版) ネイティブコードで描画 layoutlib (linux-x86_64版) ネイティブコードで描画 画像 (Mac) ローカルでの出力 画像 (CI) CI環境での出力 丸めの結果が わずかに違う 40

Slide 41

Slide 41 text

環境を近づける 環境を「近づける」のでは なく「同一にする」 ● しきい値 (imageDifferenceThreshold)で ±1を許容する手もあるが、本物の1 ピクセルの変化まで見逃してしまう Mac (arm64) ローカル画像 CI (Linux/x86_64) CI画像 ±1の差分が残る 環境を同一にする Docker (Linux/x86_64) ローカル画像 CI (Linux/x86_64) CI画像 バイト単位で一致 ※ Linux向けlayoutlibにarm64版はなく、 揃える先はLinux/x86_64一択 41

Slide 42

Slide 42 text

環境を近づける 環境を「近づける」のでは なく「同一にする」 ● だから、生成環境そのものを1つに 固定する CIと同じLinux/x86_64環境を Dockerで丸ごと手元に再現する Mac (arm64) ローカル画像 CI (Linux/x86_64) CI画像 ±1の差分が残る 環境を同一にする Docker (Linux/x86_64) ローカル画像 CI (Linux/x86_64) CI画像 バイト単位で一致 ※ Linux向けlayoutlibにarm64版はなく、 揃える先はLinux/x86_64一択 42

Slide 43

Slide 43 text

環境を近づける 環境を「近づける」のでは なく「同一にする」 Mac (arm64) ローカル画像 CI (Linux/x86_64) CI画像 ±1の差分が残る ● 再生成してもgit diffが出ない 環境を同一にする 差異を減らすのではなくゼロにする Docker (Linux/x86_64) ローカル画像 CI (Linux/x86_64) CI画像 バイト単位で一致 ※ Linux向けlayoutlibにarm64版はなく、 揃える先はLinux/x86_64一択 43

Slide 44

Slide 44 text

Dockerの遅さはコンテナ常駐 + キャッシュ永続化で解決 ● コンテナを常駐させてdocker execで実行し、 GradleキャッシュはNamed Volumeに永続化す る ● 単一モジュールならwarm 23秒で回る 44

Slide 45

Slide 45 text

Dockerの遅さはコンテナ常駐 + キャッシュ永続化で解決 Terminal ● コンテナを常駐させてdocker execで実行し、 GradleキャッシュはNamed Volumeに永続化す $ docker run -d \ .-name screenshot-container \ る -e CI=true \ -v "$MAIN_REPO":/workspace \ 23秒で回る ● 単一モジュールならwarm -v gradle-cache:/home/circleci/.gradle \ -w /workspace \ cimg/android:2025.11 \ sleep infinity 45

Slide 46

Slide 46 text

gradlewの直叩きは仕組みで禁止する ● 実行の入り口はmake update_screenshot / make validate_screenshotに一本化する ● CI以外でのgradlew直接実行は、convention pluginのガードで失敗させる ● コンテナだけがCI=trueを渡すので、スクショはコ ンテナの中でしか生成できない 46

Slide 47

Slide 47 text

gradlewの直叩きは仕組みで禁止する Terminal ● 実行の入り口はmake update_screenshot / $ ./gradlew updateDebugScreenshotTest make validate_screenshotに一本化する ================================================ ❌ Direct execution of screenshot tasks is not allowed ● CI以外でのgradlew直接実行は、convention outside CI environment. Please use one of the following commands instead: pluginのガードで失敗させる make update_screenshot (changed modules only) make update_screenshot_all (all modules) ● コンテナだけがCI=trueを渡すので、スクショはコ ================================================ ンテナの中でしか生成できない BUILD FAILED ● 47

Slide 48

Slide 48 text

Compose Screenshot Testingを運用して 見えてきた課題 01 手元のMacとCIで生成されるスクショに差分が出る 02 スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい 03 PR上で画像の差分が見えないことがある 04 ネットワーク画像が撮れない 05 Preview関数やテストクラスの命名がばらばらになる 48

Slide 49

Slide 49 text

スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい ● 失敗したテストの Reference / Actual / Diff を PRコメントの表で見せたい ● しかしGitHubには、APIから画像をアップロード する方法がない 49

Slide 50

Slide 50 text

スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい ● 失敗したテストの Reference / Actual / Diff を PRコメントの表で見せたい ● しかしGitHubには、APIから画像をアップロード する方法がない https://github.blog/changelog/2026-09-01-github-cli-media-in-issues-pull-requests-and-comments/ 50

Slide 51

Slide 51 text

スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい ● 失敗したテストの Reference / Actual / Diff を PRコメントの表で見せたい ● しかしGitHubには、APIから画像をアップロード する方法がない https://github.blog/changelog/2026-09-01-github-cli-media-in-issues-pull-requests-and-comments/ 51

Slide 52

Slide 52 text

画像の置き場所には、 2つの選択肢がある ● 外部ストレージに上げる ○ S3やGCSなどにアップロードして、そのURLを コメントに埋め込む ● リポジトリにコミットする ○ PRブランチに画像をコミットして、コミット固定 のリンク(permalink)で参照する 52

Slide 53

Slide 53 text

リポジトリにコミットして、 permalinkで参照する ● 外部ストレージは、置き場の用意、CIからのアップロー ド、閲覧側のアクセス制御まで自前で設計することになる ● コミットなら、リポジトリの権限がそのまま効く ○ コミットSHA固定なので画像が後から変わらない ● 代わりにPRブランチにコミットが積まれるので、マージ前 に消す仕組みをセットで用意する 53

Slide 54

Slide 54 text

比較画像をコミットして、 permalinkの表をコメント する ● 比較画像は、validateが出力 する実描画(rendered/)と 差分マスク(diffs/)から作る ● 消し忘れはCIのチェックとリマ インダーコメントで防ぐ 1. validate失敗 2. プラグインの成果物から 比較画像を用意 3. [skip ci]でPRブランチに コミット 4. コミットSHA固定のリンクで 表をコメント 5. マージ前にクリーンアップ 54

Slide 55

Slide 55 text

比較画像をコミットして、 permalinkの表をコメント する ● 比較画像は、validateが出力 する実描画(rendered/)と 差分マスク(diffs/)から作る ● 消し忘れはCIのチェックとリマ インダーコメントで防ぐ 1. validate失敗 2. プラグインの成果物から 比較画像を用意 3. [skip ci]でPRブランチに コミット 4. コミットSHA固定のリンクで 表をコメント 5. マージ前にクリーンアップ 55

Slide 56

Slide 56 text

比較画像をコミットして、 permalinkの表をコメント する ● 比較画像は、validateが出力 する実描画(rendered/)と 差分マスク(diffs/)から作る ● 消し忘れはCIのチェックとリマ インダーコメントで防ぐ 1. validate失敗 2. プラグインの成果物から 比較画像を用意 3. [skip ci]でPRブランチに コミット 4. コミットSHA固定のリンクで 表をコメント 5. マージ前にクリーンアップ 56

Slide 57

Slide 57 text

比較画像をコミットして、 permalinkの表をコメント する ● 比較画像は、validateが出力 する実描画(rendered/)と 差分マスク(diffs/)から作る ● 消し忘れはCIのチェックとリマ インダーコメントで防ぐ 1. validate失敗 2. プラグインの成果物から 比較画像を用意 3. [skip ci]でPRブランチに コミット 4. コミットSHA固定のリンクで 表をコメント 5. マージ前にクリーンアップ 57

Slide 58

Slide 58 text

比較画像をコミットして、 permalinkの表をコメント する ● 比較画像は、validateが出力 する実描画(rendered/)と 差分マスク(diffs/)から作る ● 消し忘れはCIのチェックとリマ インダーコメントで防ぐ 1. validate失敗 2. プラグインの成果物から 比較画像を用意 3. [skip ci]でPRブランチに コミット 4. コミットSHA固定のリンクで 表をコメント 5. マージ前にクリーンアップ 58

Slide 59

Slide 59 text

比較画像をコミットして、 permalinkの表をコメント する ● 比較画像は、validateが出力 する実描画(rendered/)と 差分マスク(diffs/)から作る ● 消し忘れはCIのチェックとリマ インダーコメントで防ぐ 1. validate失敗 2. プラグインの成果物から 比較画像を用意 3. [skip ci]でPRブランチに コミット 4. コミットSHA固定のリンクで 表をコメント 5. マージ前にクリーンアップ 59

Slide 60

Slide 60 text

Compose Screenshot Testingを運用して 見えてきた課題 01 手元のMacとCIで生成されるスクショに差分が出る 02 スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい 03 PR上で画像の差分が見えないことがある 04 ネットワーク画像が撮れない 05 Preview関数やテストクラスの命名がばらばらになる 60

Slide 61

Slide 61 text

PR上で画像の差分が見えないことがある ● GitHubのdiffビューには、画像の差分表示機 能がある ● しかしエラーで読み込めなかったり、差分 UIだけ 出て画像が表示されないことが多い ● 参照画像の変更が正しいか、 PR上で確認でき ない 61

Slide 62

Slide 62 text

PR上で画像の差分が見えないことがある ● GitHubのdiffビューには、画像の差分表示機 能がある ● しかしエラーで読み込めなかったり、差分 UIだけ 出て画像が表示されないことが多い ● 参照画像の変更が正しいか、 PR上で確認でき ない 62

Slide 63

Slide 63 text

PR上で画像の差分が見えないことがある ● GitHubのdiffビューには、画像の差分表示機 能がある ● しかしエラーで読み込めなかったり、差分 UIだけ 出て画像が表示されないことが多い ● 参照画像の変更が正しいか、 PR上で確認でき ない 63

Slide 64

Slide 64 text

PR上で画像の差分が見えないことがある ● GitHubのdiffビューには、画像の差分表示機 能がある ● しかしエラーで読み込めなかったり、差分 UIだけ 出て画像が表示されないことが多い ● 参照画像の変更が正しいか、 PR上で確認でき ない 64

Slide 65

Slide 65 text

PR作成をskillにして、 before/afterの表を自動生成する ● BeforeもAfterも、参照画像はもうリポジトリにコ ミッ トされている ● PR作成のskillがその変更を検出して、 SHA固定の permalinkで並べた表を PR本文に埋め込む https://github.com/{owner}/{repo}/blob/{SHA}/{path}?raw=true ● PRを作るだけで UI差分が載る。 diffビューが壊れて いても確認できる 65

Slide 66

Slide 66 text

PR作成をskillにして、 before/afterの表を自動生成する ● BeforeもAfterも、参照画像はもうリポジトリにコ ミッ トされている ● PR作成のskillがその変更を検出して、 SHA固定の permalinkで並べた表を PR本文に埋め込む https://github.com/{owner}/{repo}/blob/{SHA}/{path}?raw=true ● PRを作るだけで UI差分が載る。 diffビューが壊れて いても確認できる 66

Slide 67

Slide 67 text

PR作成をskillにして、 before/afterの表を自動生成する ● BeforeもAfterも、参照画像はもうリポジトリにコ ミッ トされている ● PR作成のskillがその変更を検出して、 SHA固定の permalinkで並べた表を PR本文に埋め込む https://github.com/{owner}/{repo}/blob/{SHA}/{path}?raw=true ● PRを作るだけで UI差分が載る。 diffビューが壊れて いても確認できる 67

Slide 68

Slide 68 text

PR作成をskillにして、 before/afterの表を自動生成する ● 表のケース名は、 AIがファイル名から日本語で 導出する ○ 名前で分からないときはテストコードの 中身まで読んで判断する ● Light / Darkは、ファイル名末尾の Darkで 機械的に判別できる 68

Slide 69

Slide 69 text

Compose Screenshot Testingを運用して 見えてきた課題 01 手元のMacとCIで生成されるスクショに差分が出る 02 スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい 03 PR上で画像の差分が見えないことがある 04 ネットワーク画像が撮れない 05 Preview関数やテストクラスの命名がばらばらになる 69

Slide 70

Slide 70 text

ネットワーク画像が撮れない ● coil3は、Previewやスクショテストのような Inspection Modeではネットワーク画像を取得 しない ● 商品画像のような UIは、そのままだと全部空白 で撮れてしまう 70

Slide 71

Slide 71 text

都度分岐を書くのは手間、書かなければ白いまま ● LocalInspectionModeで分岐して代わりの画 像を出せば解決するが、画像を使う画面ごとに 毎回書くのは手間 ● かといって書かなければ、 Previewは白いまま でUIのイメージがつかみにくい 71

Slide 72

Slide 72 text

Previewかどうかの判定は、 共通の画像 Composableに集約する ● 「Previewかどうか」を知っているのは 共通の画 像 Composableだけ ● Inspection Modeのときは、プレースホルダを 自動で描画する 72

Slide 73

Slide 73 text

Previewかどうかの判定は、  GmImage.kt 共通の画像 Composableに集約する @Composable fun GmImage(source: ImageSource, ...) { ● 「Previewかどうか」を知っているのは 共通の画 val isInspectionMode = LocalInspectionMode.current val showComposedPlaceholder = isInspectionMode .& source is ImageSource.Network .& 像 Composableだけ ● Inspection Modeのときは、プレースホルダを 自動で描画する source.previewResId .= null val painter = when { showComposedPlaceholder .> null ./ プレースホルダを合成して描画 source is ImageSource.Network .> if (isInspectionMode) { painterResource(requireNotNull(source.previewResId)) } else { rememberAsyncImagePainter(.* ImageRequest(source.uri) ./) } source is ImageSource.Resource .> painterResource(source.resId) else .> null } ./ ... } 73

Slide 74

Slide 74 text

Previewかどうかの判定は、  GmImage.kt 共通の画像 Composableに集約する @Composable fun GmImage(source: ImageSource, ...) { ● 「Previewかどうか」を知っているのは 共通の画 val isInspectionMode = LocalInspectionMode.current val showComposedPlaceholder = isInspectionMode .& source is ImageSource.Network .& 像 Composableだけ ● Inspection Modeのときは、プレースホルダを 自動で描画する source.previewResId .= null val painter = when { showComposedPlaceholder .> null ./ プレースホルダを合成して描画 source is ImageSource.Network .> if (isInspectionMode) { painterResource(requireNotNull(source.previewResId)) } else { rememberAsyncImagePainter(.* ImageRequest(source.uri) ./) } source is ImageSource.Resource .> painterResource(source.resId) else .> null } ./ ... } 74

Slide 75

Slide 75 text

使う側は、 ImageSourceを渡すだけ ● 画像を使う側は、 Previewのことを何も意識しな い ● Previewでも実画像を出したいときは、 previewResIdを渡せる 75

Slide 76

Slide 76 text

 ImageSource.kt 使う側は、 ImageSourceを渡すだけ ● ● sealed interface ImageSource { 画像を使う側は、 Previewのことを何も意識しな data class Network( い val uri: Uri, @DrawableRes val previewResId: Int? = null, ) : ImageSource Previewでも実画像を出したいときは、 previewResIdを渡せる data class Resource( @DrawableRes val resId: Int ) : ImageSource } 76

Slide 77

Slide 77 text

使う側は、 ImageSourceを渡すだけ ● 画像を使う側は、 Previewのことを何も意識しな い  ProductItem.kt ● Previewでも実画像を出したいときは、 GmImage( source = ImageSource.Network(imageUri), previewResIdを渡せる contentDescription = product.name, ) 77

Slide 78

Slide 78 text

Compose Screenshot Testingを運用して 見えてきた課題 01 手元のMacとCIで生成されるスクショに差分が出る 02 スクショの検証に失敗したとき、 PR上で比較差分を出すのが難しい 03 PR上で画像の差分が見えないことがある 04 ネットワーク画像が撮れない 05 Preview関数やテストクラスの命名がばらばらになる 78

Slide 79

Slide 79 text

Preview関数やテストクラスの命名がばらばらになる ● テストを書くのは AIエージェントで、しかも並列。 規則がなければ命名は書くたびに揺れていく ● Preview名は参照画像のファイル名になるの で、揺れると画像を名前で扱う仕組みが全部崩 れる 79

Slide 80

Slide 80 text

命名は、機械が読むメタデータとして設計する ● 人間の好みではなく、名前から情報を導出でき ることを基準に決める ● Preview名はスペースなしの PascalCaseで短 く。Dark側は末尾に Darkを付ける ● 追加のアノテーションは増やさず、命名だけを唯 一のキーにする 80

Slide 81

Slide 81 text

規約は、テストを書く AIに渡しておく ● 規約はテストガイドラインに 1箇所で書き、テスト 作成のskillから参照させる ● 人に周知して覚えてもらうのではなく、書き手で あるAIに規約を渡す ● 生成の入り口で揃えれば、あとから直す必要が ない 81

Slide 82

Slide 82 text

生成したスクリーンショットの活用 82

Slide 83

Slide 83 text

テストのための画像は、テスト以外にも使える ● 参照画像は今や 550枚超。18モジュール、 22 画面、277のstateを網羅している ● これはアプリの全画面・全状態のカタログそのも の 83

Slide 84

Slide 84 text

スクリーンショットギャラリー 84

Slide 85

Slide 85 text

85

Slide 86

Slide 86 text

スクリーンショットギャラリー ● 画面・コンポーネント種別・ state・Light/Dark を、すべてテストの命名から自動導出する ● 人間が編集するのは日本語ラベルのファイル 1 つだけ 86

Slide 87

Slide 87 text

スクリーンショットギャラリー  gallery_data.generated.js { ● 画面・コンポーネント種別・ state・Light/Dark "key": "product.list", "label": "商品リスト", を、すべてテストの命名から自動導出する "components": [{ "key": "ProductItem", "label": "商品アイテム", "type": "component", "states": [{ "key": "WithReviewScore", "label": "レビュースコアあり", "light": ".../ProductItemTest_ProductItemWithReviewScore_….png", "dark": ".../ProductItemTest_ProductItemWithReviewScoreDark_….png" }] }] ● 人間が編集するのは日本語ラベルのファイル 1 つだけ } 87

Slide 88

Slide 88 text

テストの参照画像 参照画像を走査して、 命名からデータを作り、 HTMLに組む ● 入力はテストの参照画像そのもの 追加の撮影も、管理用のDBもない ● 命名から導出したデータと、ビューアの テンプレートHTMLを分離する ● reference/**/*.png 命名をパースして メタデータを導出 (日本語ラベル ) ※手編集はここだけ データファイルを生成 (keyとラベルを両方含む・自動生成 ) テンプレートHTMLと組み合わせる ※テンプレートは最初に作ったらほぼ固定 同じデータから、リポジトリ参照版も画像 埋め込みの単一HTMLも生成できる ● labels.json ふだん人間が編集するのは、日本語ラ ベルのJSONだけ リポジトリ参照版HTML 軽量・ローカル閲覧用 単一HTML (画像埋め込み) 社内共有用 常設URL(Cloud Run + IAP)へ ※ mainへのマージで Actionsが再生成し、 更新PRを自動作成 88

Slide 89

Slide 89 text

テストの参照画像 参照画像を走査して、 命名からデータを作り、 HTMLに組む ● 入力はテストの参照画像そのもの 追加の撮影も、管理用のDBもない ● 命名から導出したデータと、ビューアの テンプレートHTMLを分離する ● reference/**/*.png 命名をパースして メタデータを導出 (日本語ラベル ) ※手編集はここだけ データファイルを生成 (keyとラベルを両方含む・自動生成 ) テンプレートHTMLと組み合わせる ※テンプレートは最初に作ったらほぼ固定 同じデータから、リポジトリ参照版も画像 埋め込みの単一HTMLも生成できる ● labels.json ふだん人間が編集するのは、日本語ラ ベルのJSONだけ リポジトリ参照版HTML 軽量・ローカル閲覧用 単一HTML (画像埋め込み) 社内共有用 常設URL(Cloud Run + IAP)へ ※ mainへのマージで Actionsが再生成し、 更新PRを自動作成 89

Slide 90

Slide 90 text

誰にでも配れる形にする ● 画像を埋め込んだ単一 HTML(550枚を超えて も十数MB)。ファイル 1つなので置くだけで配れ る ● Cloud Run + IAPで常設URL化して、 社内ポータルに掲載 90

Slide 91

Slide 91 text

誰にでも配れる形にする ● URLで「この画面のこの状態」を 直接指せる deep linkを共有できる 91

Slide 92

Slide 92 text

92

Slide 93

Slide 93 text

仕様書HTMLの生成 93

Slide 94

Slide 94 text

94

Slide 95

Slide 95 text

開発の起点が Figmaからプロトタイプに変わってきた 以前 今 PdMが起案・ワイヤーを作る 誰かがアイデアを起案 AIで動くプロトタイプを作る デザイナーがデザインを作る Figmaで関係者の認識合わせ デザイナー待ちで ここで詰まる PdMやエンジニアが作る デザインガイドラインは 整備している前提 PdMが動くもので確認 デザイン・仕様を Figmaに清書 プロトタイプを起点に開発 エンジニアが実装 デザイナーが細部をチェック PdMが最終確認・リリース PdMが最終確認・リリース 動くプロトタイプで認識合わせを前倒しし、入り口の詰まりが軽減された 95

Slide 96

Slide 96 text

ではその仕様は、どこに残る ? 96

Slide 97

Slide 97 text

スクショテストの画像から仕様書を生成する ● 素材はすでにある。状態を網羅した画像はスクショ テストが、仕様のテキストは IssueやNotionが持っ ている ● Snackbarのような一瞬しか出ない UIだけ、実機 キャプチャで補う ● 出力は画像埋め込みの自己完結 HTML1枚。社内 のレポートサイトに置くだけで URLで配れる 97

Slide 98

Slide 98 text

QAやiOSにも仕様を共有しやすくする ● レビュー・ QA・デザイン確認向けのビジュアル仕 様書として運用を始めている ● 同じ画面を作る iOSにも、実装済み仕様の共有 手段として広げていきたい 98

Slide 99

Slide 99 text

テストの画像が、カタログにも仕様書にもなる ● アプリ全体のカタログ (ギャラリー )と、 案件単位の仕様書 (仕様書HTML) ● どちらも素材は同じ。スクショテストを運用してい れば、あとは組み合わせるだけ 99

Slide 100

Slide 100 text

動作確認と、 AIに任せるための土台 100

Slide 101

Slide 101 text

スクショテストが守るのは、静的な見た目まで ● 操作フローや画面遷移など、動きの確認はスク ショテストではカバーできない 101

Slide 102

Slide 102 text

動作確認は、 AIエージェントとペアでやる ● PR本文・Issue・差分から、 AIが「操作 →期待される 観測結果」の検証計画を立てる ● 人間は指示された操作をタップするだけ。判定はロ グ・スクショ・ DBの証拠を読んで AIがやる ● 実際に確認できた項目だけを、 PRのチェックリスト に反映する 102

Slide 103

Slide 103 text

全自動にすることもできる。あえて人間を挟んでいる ● 人間が操作するから、機械では判別できない UXの 違和感に気づける ● AIとの要件の認識違いも、その場で見つけてカ バーできる ● 見つけた不具合は、その流れで AIにIssueを作成さ せる 103

Slide 104

Slide 104 text

Android Studioを、ほぼ立ち上げなくなった ● UIの確認は、 PR上のスクショで完結する ● 動作の確認は AIとのペア検証で、ビルドもデプ ロイもAIが実行する ● ここまで揃うと、開発がターミナルとエミュレータ だけでほぼ回る 104

Slide 105

Slide 105 text

並列で任せられるのは、設計が揃っているから ● 依存が絡み合っていると、 AIは1つの修正のた めに広い範囲を読む ● 設計が統一されていて、お手本になるコードが あれば、近くを見るだけで書ける ● マルチモジュールなら、触った機能だけをビルド してテストできる 105

Slide 106

Slide 106 text

AIに任せられるのは、 規約を育てているから 1. AIが実装する ● 静的解析で直せるものは、 2. 人間がレビューで 指摘する ktlint + spotlessで 機械的に整える 同じ指摘を繰り返しにくくなる ● 人間がAIにした指摘は、規 約に反映して返す ○ この規約更新も skillで 4. セルフレビューが 規約に照らして直す 3. 指摘を規約に 反映する Copilotレビューと2段構え 規約更新もskillで習慣化 習慣化している 106

Slide 107

Slide 107 text

AIに任せられるのは、 規約を育てているから 1. AIが実装する ● 規約は一発では守られない 2. 人間がレビューで 指摘する ○ claudeのサブエージェント によるセルフレビューと 同じ指摘を繰り返しにくくなる Copilotレビューの 2段構え で、規約に照らして直す 4. セルフレビューが 規約に照らして直す 3. 指摘を規約に 反映する Copilotレビューと2段構え 規約更新もskillで習慣化 107

Slide 108

Slide 108 text

規約はdocsに、実装の手順はスキルに書く ● 規約の実体は docs配下に領域別に配置 ● CLAUDE.md / AGENTS.md はその索引で、必 要な領域を必要なときに読み出させる 108

Slide 109

Slide 109 text

規約はdocsに、実装の手順はスキルに書く ● 実装の手順はレイヤーや種類ごとにスキルへ ○ Compose UI・ViewModel・UseCase・テスト ・計測イベント etc… ● スキルからは規約を参照させる ● レビューで使うのは規約、実装で使うのはスキ ル 109

Slide 110

Slide 110 text

まとめ ● 実装は並列になったのに、確認は直列のまま。 これがAIエージェント時代の詰まりどころだった ● Compose Screenshot TestingでPreviewを画 像にして、 UIの確認を PR上で完結させた 110

Slide 111

Slide 111 text

まとめ ● 生成環境と実行方法は仕組みで強制し、 命名は書き手の AIに規約として渡す ● 溜まった参照画像は、アプリ全体のカタログに も、案件単位の仕様書にもなる 111

Slide 112

Slide 112 text

UI仕様を「見えるもの」にする Compose Screenshot Testing とギャラリーで支 える、AIエージェント時代の Android UI開発 Taichi Sato / @syarihu Giftmall, Inc. Android Engineer 112