前提条件
- 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
)
}
}
右側に出てくる
引数を変えてみる
中身が見える
上の方に緑で書かれているのが公式の説明だからこれを読もう
要はこのパラメータが設定できる
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)
}
}
}
プレビュー上で動かす
指マークのところを押す
ビルドしないでもエディタ上で動きが見れる
TextField
とかButton
とか入れておけば動きは分かりそう
使っている@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を作成する
@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
@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を変えたアノテーションを作るのが良さそう
意味不明だからバグであってそのうち直ってくれ
@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
サンプルに出していたアプリはストアに公開しているので、ぜひダウンロードしてください
GooglePlay
GitHub