3
1

Jetpack Compose の @Preview を使いこなす

Last updated at Posted at 2024-07-15
1 / 42

前提条件

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

単体で使ってみる


HolloWorld


NewProjectした時に出てくるやつ

// 表示したい内容を記述して、
@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


引数を変えてみる


Commandを押しながら@Previewをクリック
スクリーンショット 2024-07-15 19.44.40.png


中身が見える
上の方に緑で書かれているのが公式の説明だからこれを読もう
スクリーンショット 2024-07-15 19.46.05.png


要はこのパラメータが設定できる

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
)

@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つがある

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

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

公式ブログ


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

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

PreviewParameterを使う


複数の引数を入れたい時に使える

// ここに配列を作る
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


プレビュー上で動かす


指マークのところを押す
ビルドしないでもエディタ上で動きが見れる
TextFieldとかButtonとか入れておけば動きは分かりそう
スクリーンショット 2024-07-15 20.18.42.png


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


Themeとか入っていないことがほとんどだろうから実際使うことはそうないだろうけど
Greetingにデフォルト引数を設定してプレビューもつける
このGreetingを呼び出して実際のアプリを作ってもいい

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

実際のパターン

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


Previewが出ない


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


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

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

データベースの内容が確定していないので動かない


解決策

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

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

未確定なところは引数で渡す
たぶん依存性注入ってやつ

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


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


もうちょっと汎用的にする

Interfaceを作成する

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

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

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

OS機能を使っている(?)


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

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

解決策

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

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

@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() // 通知権限の取得
                }
            }
        },
    ) 
}

一部省略しているので実際のコードをどうぞ


ScreenShot Test で Flow に値が入ってない


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


解決策

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

@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

@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

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

@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を変えたアノテーションを作るのが良さそう
意味不明だからバグであってそのうち直ってくれ

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


サンプルに出していたアプリはストアに公開しているので、ぜひダウンロードしてください
GooglePlay
GitHub

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1