Jetpack Compose の @Preview を使いこなす

# 前提条件
- Jetpack Compose を使ったことがある
- Androidアプリを作ったことがある
- Hiltが分かると良い わからなければ雰囲気で読み取って

---
# 単体で使ってみる

---
## HolloWorld
---
NewProjectした時に出てくるやつ
```kotlin
// 表示したい内容を記述して、
@Preview // ←これをつけるだけ
@Composable
fun GreetingPreview() {
    MyApplicationTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            Greeting("Android")
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Card {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}
```

---
右側に出てくる

![スクリーンショット 2024-07-13 21.46.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620387/4f5d2d9a-11b7-d069-c53a-5b49dba4d6d0.png)

---
## 引数を変えてみる
---
Commandを押しながら`@Preview`をクリック
![スクリーンショット 2024-07-15 19.44.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620387/fc29c926-5cf2-ca91-aaa2-dc43dddf2841.png)

---
中身が見える
上の方に緑で書かれているのが公式の説明だからこれを読もう
![スクリーンショット 2024-07-15 19.46.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620387/9d653f1a-5441-af06-4f6d-735c2b1c18dd.png)

---
要はこのパラメータが設定できる
```kotlin
annotation class Preview(
    val name: String = "", // 表示名
    val group: String = "", // 複数のプレビューをグループ化して、このグループだけ表示、とかに使う
    @IntRange(from = 1) val apiLevel: Int = -1, // OSバージョン デフォルトでは最新になるのかな？
    val widthDp: Int = -1, // 画面横サイズ
    val heightDp: Int = -1, // 画面縦サイズ
    val locale: String = "", // 言語コード 日本語ならJa 英語ならEn みたいなやつ
    @FloatRange(from = 0.01) val fontScale: Float = 1f, // フォントサイズ
    val showSystemUi: Boolean = false, // ベゼルとかの画面の外側見せる
    val showBackground: Boolean = false, // 
    val backgroundColor: Long = 0, // 背景色
    @UiMode val uiMode: Int = 0, // ダークモード
    @Device val device: String = Devices.DEFAULT, // 端末
    @Wallpaper val wallpaper: Int = Wallpapers.NONE, // DynamicColor
)
```
---

```kotlin:例
@Preview // プレビューは複数並べられる
@Preview(locale = "En") // 英語
@Preview(fontScale = 2f) // フォントサイズ大きいパターン（Androidの最大設定が2f）
@Preview(widthDp = 320, heightDp = 600) // 画面サイズ小さいパターン（数値は適当）
@Composable
fun GreetingPreview() {
    MyApplicationTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            Greeting("Android")
        }
    }
}
```


---
## `Compose Multipreview templates`を使う
---
複数のプレビューがまとまったやつ
以下の4つがある

```kotlin
@PreviewScreenSizes // 画面サイズ詰め合わせ
@PreviewFontScale // フォントサイズ詰め合わせ
@PreviewLightDark // ライトテーマとダークテーマ
@PreviewDynamicColors // 赤青緑黄の4色のDynamicTheme
```

`@Preview`と違って引数入れられないので意外と使いづらい

[公式ブログ](https://android-developers.googleblog.com/2023/11/android-studio-hedgehog-is-stable.html#:~:text=information%20in%20Debugger-,Compose%20Multipreview%20templates,-Android%20Studio%20Hedgehog)

---
Greetingだと違いが分かりづらいだろうけどその辺は自分で調整して

```kotlin:例
@PreviewScreenSizes
@PreviewFontScale
@PreviewLightDark
@PreviewDynamicColors
@Composable
fun GreetingPreview() {
    MyApplicationTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            Greeting("Android")
        }
    }
}
```

---
## `PreviewParameter`を使う
---
複数の引数を入れたい時に使える


```kotlin
// ここに配列を作る
class PreviewProviderSample : PreviewParameterProvider<String> {
    override val values: Sequence<String>
        get() = sequenceOf(
            "Android",
            "World",
            "Preview"
        )
}

@Preview
@Composable
fun PreviewParameterPreview(
    @PreviewParameter(PreviewProviderSample::class) title: String,
) {
    MyApplicationTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            Greeting(title)
        }
    }
}
```

![スクリーンショット 2024-07-22 10.39.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620387/dfaaf8de-c862-f342-731b-821bc1bc6c61.png)

---
## プレビュー上で動かす
---
指マークのところを押す
ビルドしないでもエディタ上で動きが見れる
`TextField`とか`Button`とか入れておけば動きは分かりそう
![スクリーンショット 2024-07-15 20.18.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620387/53245abe-8678-a8be-fa17-6d9378eaebf2.png)

---
## 使っている@Composableに直接@Previewを入れてもいい
---

Themeとか入っていないことがほとんどだろうから実際使うことはそうないだろうけど
Greetingにデフォルト引数を設定してプレビューもつける
このGreetingを呼び出して実際のアプリを作ってもいい
```kotlin
@Preview
@Composable
fun Greeting(name: String = "title", modifier: Modifier = Modifier) {
    Card {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}
```
---


# 実際のパターン
私の遭遇したうまくいかねーなーってとこと現状の解決方法です
もっといいやり方は常時募集してます

---
## Previewが出ない

---
### 未確定な要素が入っている

---
```kotlin: 動かない例
@Composable
fun Screen() {
    ...
    val title = database.getTitle() // この先でデータベースから取得する
    TopAppBar(
        title = { Text(text = title) },
    )
}

@Preview
@Composable
fun PreviewScreen() {
    Screen()
}
```
---
データベースの内容が確定していないので動かない

---
#### 解決策

```kotlin: 引数で渡す
@Composable
fun Screen(title: String) { // 引数で渡す
    ...
    // val title = database.getTitle() // 消す
    TopAppBar(
        title = { Text(text = title) },
    )
}

@Preview
@Composable
fun PreviewScreen() {
    Screen("タイトル") // 引数で渡す
}
```
---
未確定なところは引数で渡す
たぶん依存性注入ってやつ

Previewを使っていると強制的に疎結合になるのはいい仕組みだ

---
ただ、これだと引数が相当増える上に、複数回呼ぶときに毎回プレビュー用の値を入れ直さないといけないので大変でプレビューを使いたくなくなる

---
#### もうちょっと汎用的にする
Interfaceを作成する

```kotlin: UI
@Composable
fun Screen(viewModel: ISampleViewModel) {
    TopAppBar(
        title = { Text(text = viewModel.title) },
    )
}

@Preview
@Composable
fun PreviewScreen() {
    Screen(PreviewSampleViewModel)
}

// 実際に使うときの例
@Composable
fun ProductionScreen() {
    val viewModel = hiltViewModel<SampleViewModel>()
    Screen(viewModel)
}
```

---
#### デフォルト値を入れる
Interface作りたくなかったらデフォルト値入れておくのもありかな
複数回使いたい時もサッと出せる
入れ忘れがあるからどうかなとは思うけど選択肢としては

```kotlin: 引数で渡す
@Composable
fun Screen(title: String = "タイトル") { // 引数で渡す
    ...
    // val title = database.getTitle() // 消す
    TopAppBar(
        title = { Text(text = title) },
    )
}

@Preview
@Composable
fun PreviewScreen() {
    Screen("タイトル") // 引数で渡す
}
```

---
### OS機能を使っている（？）

---
こっちは正直なんでかよくわかってないんだけど、パーミッションの取得のタイミングで動かなかった

```kotlin
@Composable
fun NotificationButton() {
    val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
    IconButton(
        onClick = {
            notificationPermissionState.launchPermissionRequest() // 通知権限の取得
        },
    ) 
}
```

---
#### 解決策

プレビューかどうかを取得して、プレビューの時だけ動かす

引数にisPreviewを入れると、結構いろんなところでプレビューかどうかを持たないといけなくなるのが嫌なので、この部品内で完結できるようにした
もっといいやり方募集中

```kotlin
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NotificationButton() {
    // プレビューモードでなければ、通知権限の状態を取得
    val isPreview = LocalInspectionMode.current
    val notificationPermissionState = if (!isPreview) {
        rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
    } else null // プレビュー時はnull

    IconButton(
        onClick = {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                if (!notificationPermissionState?.status?.isGranted!!) {
                    notificationPermissionState.launchPermissionRequest() // 通知権限の取得
                }
            }
        },
    ) 
}
```
一部省略しているので[実際のコード](https://github.com/yuki-kamikita/QuickTodo/blob/1978ab74fed54df80123e44f43c53f3df36db191/app/src/main/java/com/akaiyukiusagi/quicktodo/ui_layer/screen/HomeScreen.kt#L361-L379)をどうぞ

---
## [ScreenShot Test](https://developer.android.com/studio/preview/compose-screenshot-testing) で Flow に値が入ってない

---
初期化してすぐにスクリーンショットを撮影しているようなので、Flowの値が更新される前に撮影が完了してしまう

---
#### 解決策

Flowの初期値にプレビューで表示したい値を入れる

```kotlin
@Composable
fun Screen(viewModel: ISampleViewModel) {
    val title by viewModel.title.collectAsState(initial = viewModel.initialTitle) // ここの値がスクリーンショットに撮影される
    TopAppBar(
        title = { Text(text = title) },
    )
}

@Preview
@Composable
fun PreviewScreen() {
    Screen(PreviewSampleViewModel)
}

interface ISampleViewModel {
    val initialTitle: String
    val title: Flow<String>
}

class PreviewSampleViewModel: ISampleViewModel {
    override val initialTitle: String = "タイトル" // 全ての箇所でここを参照するようにする
    override val title: Flow<String> = flowOf(initialTitle)
}
```

---
## アノテーションを自作する

---
`@PreviewDynamicColors`とかのコードを読んで真似して、クラス名を変えて、プレビューを好きなもの入れればok

![スクリーンショット 2024-07-16 0.03.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620387/356df2ba-0cde-7762-5539-76cc49008452.png)

```kotlin
@Retention(AnnotationRetention.BINARY)
@Target(
        AnnotationTarget.ANNOTATION_CLASS,
        AnnotationTarget.FUNCTION
)
@Preview(name = "Red", wallpaper = RED_DOMINATED_EXAMPLE)
@Preview(name = "Blue", wallpaper = BLUE_DOMINATED_EXAMPLE)
@Preview(name = "Green", wallpaper = GREEN_DOMINATED_EXAMPLE)
@Preview(name = "Yellow", wallpaper = YELLOW_DOMINATED_EXAMPLE)
annotation class PreviewDynamicColors
```

---
使いそうな好きな組み合わせでアノテーションを作ってどこかに書いておく

```kotlin: 例
@Retention(AnnotationRetention.BINARY)
@Target(
        AnnotationTarget.ANNOTATION_CLASS,
        AnnotationTarget.FUNCTION
)
@Preview // プレビューは複数並べられる
@Preview(locale = "En") // 英語
@Preview(fontScale = 2f) // フォントサイズ大きいパターン（Androidの最大設定が2f）
@Preview(widthDp = 320, heightDp = 600) // 画面サイズ小さいパターン（数値は適当）
annotation class PreviewTemplate
```

---
# その他

---
## `@PreviewDynamicColors`が効かない
なぜか色が全部青になる

`apiLevel = 34`だと機能していないらしい
今のところ、自力でapiLevelを変えたアノテーションを作るのが良さそう
意味不明だからバグであってそのうち直ってくれ

```kotlin:apiLevel = 34 にした例
@Retention(AnnotationRetention.BINARY)
@Target(
    AnnotationTarget.ANNOTATION_CLASS,
    AnnotationTarget.FUNCTION
)
@Preview(name = "Blue", apiLevel = 33, wallpaper = BLUE_DOMINATED_EXAMPLE)
@Preview(name = "Red", apiLevel = 33, wallpaper = RED_DOMINATED_EXAMPLE)
@Preview(name = "Green", apiLevel = 33, wallpaper = GREEN_DOMINATED_EXAMPLE)
@Preview(name = "Yellow", apiLevel = 33, wallpaper = YELLOW_DOMINATED_EXAMPLE,)
annotation class PreviewDynamicColorsApi33
```

[stack overflow](https://stackoverflow.com/questions/78278321/dynamic-color-preview-not-working-in-android-studio)

---

サンプルに出していたアプリはストアに公開しているので、ぜひダウンロードしてください
[GooglePlay](https://play.google.com/store/apps/details?id=com.akaiyukiusagi.quicktodo)
[GitHub](https://github.com/yuki-kamikita/QuickTodo)