Compose Previewのスクリーンショットテストの今の状況とComposeのPreviewがIDE上でどうやって作られるか

Google I/Oで発表された、まだリリースされていない機能ですが、一番気になっている機能でもあります。
個人的に[Roborazzi](https://github.com/takahirom/roborazzi)というAndroid端末を使わずにスクショを取れるライブラリを作っているので、その両方の将来性みたいなところでも気になるところです。

## 確認方法
まだ未リリースの機能ではあるのですが、ソースコードはmainブランチ(mirror-goog-studio-main)に入ったりしており、見ていくことができます。

コミットでいうと `13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41` 時点での情報をお知らせします。
Android Gradle PluginはAOSPの以下にあるようです。
https://android.googlesource.com/platform/tools/base/+/13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41/build-system

以下のmirror-goog-studio-mainブランチで開発が継続的にされているようです。
https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-main/build-system

## スクリーンショットテストのテストを確認して挙動を確認する

まず何にしても使い方を知るにはテストを確認するとわかりやすい気がしています。

https://android.googlesource.com/platform/tools/base/+/13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41/build-system/integration-test/application/src/test/java/com/android/build/gradle/integration/testing/screenshot/ScreenshotTest.kt

まとめると以下です

* 事前条件
    * **androidTest/にPreview関数の付いたComposable関数を置く**
    * **android.experimental.enableScreenshotTest** を有効にする
* **screenshotTestDebugAndroidTest --record-golden** でComposable関数のPreviewのスクショを撮る

ちょっとなんでかはわからないですが、**デバイスで動かすわけではないのにandroidTestに置く**ようです (今後変わるかな？)

## スクリーンショットテストのタスクを確認する

ここでどういう仕組みで動いているのか気になってきますよね？
タスクのコードは以下になります。

https://android.googlesource.com/platform/tools/base/+/13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/ScreenshotTestTask.kt


* JUnitPlatformTestFrameworkを使う
* JUnitPlatformTestFrameworkのincludeEngines("screenshot-test-engine")によってそのエンジンを使ってテストを実行するEngineを選ぶ
* dependencyにengineである **"com.android.tools.screenshot:junit-engine" (未リリース) を追加**する ※1
* エンジンを使ってテストを動かす

**ちなみにこのjunit-engineはまだmaven.google.comにないようなので、実際に動かそうとすると失敗します。**


※1 

```kotlin
                val engineVersion = "0.0.1" +
                        if (Version.ANDROID_GRADLE_PLUGIN_VERSION.endsWith("-dev"))
                            "-dev"
                        else
                            "-alpha01"
                dependencies.add(
                        configurationName,
                        "com.android.tools.screenshot:junit-engine:${engineVersion}")
            }
```

## screenshot:junit-engineを確認する

Engineのコードはこちらになります。

https://android.googlesource.com/platform/tools/base/+/13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41/screenshot/junit-engine/src/main/java/com/android/tools/screenshot/junit/engine/ScreenshotTestEngine.kt

* previewJarのMainを動かす。
    * Preview関数のあるソースコード一覧。output pathやgolden pathなどを渡す。
    * previewJarはスクリーンショットテストのタスクで設定している ※2

※2

```kotlin
                container.create(previewlibCliToolConfigurationName).apply {
                    isVisible = false
                    isTransitive = true
                    isCanBeConsumed = false
                    description = "A configuration to resolve PreviewLib CLI tool dependencies."
                }
                dependencies.add(
                        previewlibCliToolConfigurationName,
                        "com.android.screenshot.cli:screenshot:${Version.ANDROID_TOOLS_BASE_VERSION}")
```

## screenshot.cli:screenshotを確認する


コード:
https://android.googlesource.com/platform/tools/base/+/13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41/previewlib/cli/src/main/java/com/android/screenshot/cli/Main.kt

* 自分自身のjarを解凍する。 (ちょっとなぜこれが必要なのかよく分からなかった)
* ComposeApplication.setupEnvVars(argumentState.extractionDir)なるものを呼ぶ
    * [どうやらこのクラスはいろんなセットアップをしている模様](https://android.googlesource.com/platform/tools/base/+/13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41/previewlib/cli/src/main/java/com/android/screenshot/cli/ComposeApplication.kt)
* **ScreenshotProvider.verifyScreenshot(findPreviewNodes(projects[0], argumentState.filePath!!)を呼び出す**


## ComposeのPreviewはどうやって作られるのか？

ScreenshotProviderは以下にあります。
 https://android.googlesource.com/platform/tools/base/+/13c6af9acff44468927e0e8e0c7f6bdb1e6f4d41/previewlib/cli/src/main/java/com/android/screenshot/cli/ScreenshotProvider.kt

ここではComposePreviewElementというものを呼び出しています。なんだか**xmlという単語結構でてきます**。
**[ComposePreviewElementはandroid/tools/idea/にあるもの](https://cs.android.com/android-studio/platform/tools/adt/idea/+/mirror-goog-studio-main:compose-designer/src/com/android/tools/idea/compose/preview/ComposePreviewElement.kt;l=362;drc=7866e6c5c68637a1197bec34aa0c821909290900)なので、Android Studio側で使われる実装だと思われます。**

**ComposePreviewElementのコードは以下のようになっています。xmlでComposeを作ることによってPreviewを表示していそうということがわかりました。**

```kotlin
  override fun toPreviewXml(): PreviewXmlBuilder {
    val matchParent = displaySettings.showDecoration
    val width =
      dimensionToString(
        configuration.width,
        if (matchParent) SdkConstants.VALUE_MATCH_PARENT else VALUE_WRAP_CONTENT
      )
...
    val xmlBuilder =
      PreviewXmlBuilder(COMPOSE_VIEW_ADAPTER_FQN)
        .androidAttribute(ATTR_LAYOUT_WIDTH, width)
        .androidAttribute(ATTR_LAYOUT_HEIGHT, height)
...
        .toolsAttribute("composableName", composableMethodFqn) // ← ここでComposeの関数の場所を渡す

...
    return xmlBuilder
  }
```

で、このComposeを表示しているAndroid　Viewは実際にどこなのか？が疑問になります。

コードを追うと `androidx.compose.ui.tooling.ComposeViewAdapter` というクラスのようです。

そのコードは以下にあります。FrameLayoutを継承したViewになっています。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt;l=129;drc=0955f484eb57e6e892afe8fbe0c796060dcda463

**実際に中では以下のようにComposable関数を呼び出しているようです。**

```kotlin
ComposableInvoker.invokeComposable(
    className,
    methodName,
    composer,
    *getPreviewProviderParameters(parameterProvider, parameterProviderIndex)
)
```

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt;l=499-526;drc=0955f484eb57e6e892afe8fbe0c796060dcda463

# まとめ

個人的にはなかなか冒険感があって面白かったです。

* 現状はスクリーンショットテストは、なぜかandroidTest sourcesetにPreview関数を置く形で実装する
* Android StudioのPreviewがベースになって実装されているのでPreview以上のことは難しそうに見えた
    * (なのでRobolectricによってEspresso、Hiltなどと一緒に使えるRoborazziとは競合しなさう)
* ComposeのPreviewがxmlのPreviewによって実現されていた