ITmob-Ly
发布于 2023-01-10 / 543 阅读
0

Jetpack Compose 的可组合项预览详解(@Preview)

Jetpack Compose composable preview

1. 简介

Android Studio 带来了许多关于 Jetpack Compose 的新功能。本文将总结一下 JetpackCompose @Preview (可组合项函数的预览)的用法,怎样使用 @Preview 来提高我们的开发效率。

使用 @Preview 可以在 Android Studio 中预览 Jetpack Compose 组件,而无需将应用部署到设备或模拟器。也可以对某个可组合函数进行多次预览,每次预览采用不同的宽度和高度限制、字体缩放或主题背景。在开发应用的过程中,预览会随之更新,帮助更快地检查更改。

要使用 @Preview,Android Studio 的最低版本要求:Android Studio Dolphin

要启用 Android Studio 关于 Jetpack Compose 的特定功能,需要在应用程序 build.gradle 文件中添加这些依赖项:

implementation "androidx.compose.ui:ui-tooling-preview:1.3.2"
debugImplementation "androidx.compose.ui:ui-tooling:1.3.2"

使用 Android Studio 的 Compose 应用模板创建项目时,这些依赖项会自动添加。

Preview 的优势和最佳实践

使用 @Preview 可组合项的主要好处之一是避免依赖 Android Studio 中的模拟器。可以节省模拟器启动时的大量内存,以便进行更多的外观更改,以及 @Preview 轻松创建和测试小代码更改的能力。

要最有效地利用 @Preview 注解,请确保根据屏幕接收的输入状态和输出的事件来定义屏幕。除了改进的可测试性,页面可以在预览中轻松呈现!

2. 布局预览基本用法

我们先介绍不带参数的布局预览的基本用法,编写一个简单的不带参数的可组合函数,并添加 @Preview 注解。当构建应用后,预览函数的界面会显示在 Android Studio 的 Preview 视图中。

怎样在 Preview 中使用参数,请参考下文中:5. @PreviewParameter 参数的用法和怎样支持大量示例数据

如创建一个向用户显示问候语的可组合函数:

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name")
}

如需预览它的界面,编写一个不带参数的可组合函数,并向该可组合函数添加 @Preview 注解:

@Preview
@Composable
fun PreviewGreeting() {
    Greeting(name = "ITmob.cn")
}

预览窗口显示了由 @Preview 注释的可组合项生成的界面

代码发生变化时,点击预览窗口中的 刷新 按钮更新预览。如果需要重新构建项目才能更新预览,预览窗口将说明是否需要重新构建。

也可以通过向 @Preview 注释传递参数来自定义预览。例如,默认情况下,预览的标签是根据预览函数的名称而定,如上面例子中为 PreviewGreeting。您可以通过传递 name 参数来更改标签:

@Preview(name = "ITmob greeting")
@Composable
fun PreviewGreeting() {
    Greeting("ITmob.cn")
}

也可以定义多个预览函数,这些函数会全部显示于预览窗口中。例如,编写用于测试不同输入内容的函数:

@Preview(name = "Long greeting")
@Composable
fun PreviewLongGreeting() {
    Greeting("my valued friend, whom I am incapable of "
    + "greeting without using a great many words")
}

@Preview(name = "Newline greeting")
@Composable
fun PreviewNewlineGreeting() {
    Greeting("world\nwith a line break")
}

Jetpack Compose multi preview

@Preview 可传递参数的完整列表,请参阅 [@Preview](https://developer.android.com/reference/kotlin/androidx/compose/ui/tooling/preview/Preview?hl=zh-cn) 参考文档。

3. 预览注解类 Preview 介绍

在 Android Studio 中的 @Preview 注解上悬停鼠标或 “Ctrl + 单击” 查看源码,可以查看定义预览时可以调整的完整参数列表。

annotation class Preview(
    // 此预览的显示名称
    val name: String = "",
    //  此 @Preview 的组名称
    val group: String = "",
    // 渲染时使用的 API 级别
    @IntRange(from = 1) val apiLevel: Int = -1,
    // 限制渲染视口的大小
    val widthDp: Int = -1,
    // 限制渲染视口的大小
    val heightDp: Int = -1,
    // 区域设置,对应于区域设置资源限定符(如:en, fr, en-rUS)。默认使用默认文件夹。
    // 参见:https://developer.android.com/guide/topics/resources/providing-resources#LocaleQualifier
    val locale: String = "",
    // 相对于字体基本密度换算的缩放
    @FloatRange(from = 0.01) val fontScale: Float = 1f,
    // 如果为 true,将显示设备的状态栏和操作栏
    val showSystemUi: Boolean = false,
    // 如果为 true,则将使用默认背景色。
    val showBackground: Boolean = false,
    // 背景的 32 位 ARGB 颜色,如果未设置,则为 0
    val backgroundColor: Long = 0,
    // ui模式,根据 android.content.res.Configuration.uiMode
    @UiMode val uiMode: Int = 0,
    // 在预览中使用的设备
    @Device val device: String = Devices.DEFAULT
)

4. @Preview 的功能

Android Studio 提供了一些可组合项预览的扩展功能。如:更改其容器设计、与容器交互或将其直接部署到设备或模拟器。

4.1、4.2 介绍了 Android Studio 提供的对于预览功能的支持(预览部署、互动式预览);

4.3 介绍了 本地检查模式(LocalInspectionMode)的作用和用法;

其他主要介绍了 Preview 各种参数的用法;

4.1 将预览部署到设备或模拟器

Jetpack Compose deploy button

如需将预览部署到设备或模拟器,请点击 部署 按钮。Android Studio 将创建一个包含该函数生成的界面的新 Activity,并将其部署到设备上您的应用中。这样无需重新安装整个应用并导航至其该界面所在位置,即可在实际设备上试用该界面。

Jetpack Compose preview deploy

4.2 互动式预览

Android Studio 提供了互动式预览模式。当处于互动式预览模式下时,可以点击或输入界面元素,而界面会像在已安装的应用中那样进行响应。如需打开互动式预览,请点击任意预览窗口上的 互动式 按钮。预览面板将切换为该预览函数的互动模式,直到您退出该模式为止。

Jetpack Compose interactive mode

例如,响应用户点击的函数:

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(
        onClick = { updateCount(count + 1) },
        colors = ButtonDefaults.buttonColors(
            containerColor = if (count > 5) Color.Green else MaterialTheme.colorScheme.primary
        )
    ) {
        Text("I've been clicked $count times")
    }
}
@Preview
@Composable
fun PreviewCounter() {
    val counterState = remember { mutableStateOf(0) }

    Counter(
        count = counterState.value,
        updateCount = { newCount ->
            counterState.value = newCount
        }
    )
}

Jetpack Compose interactive mode demo

  • 这个示例来自官方文档,但使用 Material 3 做了修改

4.3 本地检查模式(LocalInspectionMode)

可以从 CompositionLocal 类型的 LocalInspectionMode 查看可组合项是否是在预览中呈现(在可检查组件内)。如果是在预览中呈现,则 LocalInspectionMode.currenttrue

这允许您自定义预览;例如,您可以在预览窗口中显示占位符图像而不是显示真实数据。

if (LocalInspectionMode.current) {
    // Show this text in a preview window:
    Text("Hello preview user!")
} else {
    // Show this text in the app:
    Text("Hello $name!")
}

比如加载图片的 Coil 扩展库的 AsyncImagePainter 源码:

// If we're in inspection mode skip the image request and set the state to loading.
// 如果是检查模式/预览模式,则不加载图片而是显示正在加载状态
if (isPreview) {
    val request = request.newBuilder().defaults(imageLoader.defaults).build()
    updateState(State.Loading(request.placeholder?.toPainter()))
    return
}

再比如 Google 的 accompanist 扩展库中的 WebView 也有类似用法:

AndroidView 不支持预览,可以使用 LocalInspectionMode 做判断不去渲染 AndroidView

val runningInPreview = LocalInspectionMode.current

BoxWithConstraints(modifier) {
    AndroidView(
        factory = { context ->
            (factory?.invoke(context) ?: WebView(context)).apply {
                onCreated(this)
        ...
        }
) { view ->
        // AndroidViews are not supported by preview, bail early
        if (runningInPreview) return@AndroidView

        when (val content = state.content) {
            is WebContent.Url -> {
                val url = content.url

                if (url.isNotEmpty() && url != view.url) {
                    view.loadUrl(url, content.additionalHttpHeaders.toMutableMap())
                }
            }
...

预览视图是直接在 Android Studio 中运行的并不是在 Android 设备或模拟器中运行,所以,自定义可组合项时,需要使用预览视图时,对网络访问和本地文件的访问可以通过 LocalInspectionMode 在预览窗口中显示占位符或示例数据。

4.4 多重预览注解(Multipreview

使用多重预览注解,可以定义一个注释类,该注释类本身具有具有多个不同配置的 @Preview 注释。将此注释添加到可组合函数会自动一次呈现所有不同的预览。

例如,您可以使用此批注同时预览多个设备、字体大小或主题,而无需为每个可组合项重复这些预览定义。
示例:创建自定义注解类:

// 自定义支持预览两种不同字体缩放效果的注解
@Preview(
    name = "small font",
    group = "font scales",
    fontScale = 0.5f
)
@Preview(
    name = "large font",
    group = "font scales",
    fontScale = 1.5f
)
annotation class FontScalePreviews

可以将此自定义注解用于预览可组合项:

@FontScalePreviews
@Composable
fun PreviewHelloWorld() {
    Text("Hello ITmob.cn")
}

Jetpack Compose multipreview

多个多重预览注解和普通预览注解可以组合使用,以创建一组更完整的预览。组合多重预览注解并不是着显示它们所有不同的组合。而是每个多重预览注释都独立运行,并且仅呈现其自己的变体。

示例:

// 三种设备的多重预览
@Preview(
    name = "foldable",
    group = "devices",
    device = Devices.FOLDABLE
)
@Preview(
    name = "phone",
    group = "devices",
    device = Devices.FOLDABLE
)
@Preview(
    name = "tablet",
    group = "devices",
    device = Devices.FOLDABLE
)
annotation class DevicePreviews

// dark theme 的普通预览
@Preview(
    name = "dark theme",
    group = "themes",
    uiMode = UI_MODE_NIGHT_YES
)
@FontScalePreviews    // 之前的示例中定义的两种不同字体缩放效果的多重预览
@DevicePreviews
annotation class CombinedPreviews

@CombinedPreviews
@Composable
fun PreviewHelloWorld() {
    MyTheme {
        Surface {
            Text("Hello Itmob.cn")
        }
    }
}

Jetpack Compose multipreview mix

正如上文提到的多个多重预览和普通预览组合使用时,它们不会像 Gradle 多渠道打包(product flavors)那样 flavorDimensionsproductFlavors 会组合成不同的组合,而是各个多重预览和普通预览独立运行(如:效果图中预览视图呈现了6个预览:dark theme、三个不同设备的多重预览、两个不同字体缩放的多重预览)。

4.5 代码导航和可组合项轮廓

将鼠标悬停在预览视图上可以查看其中包含的可组合项的轮廓。单击可组合项的轮廓会触发编辑器视图导航到定义它的代码。

Jetpack Compose outline

4.6 复制@Preview渲染

每个可组合项渲染的预览都可以通过 右键 - Copy Image 来复制渲染后的图像。

Jetpack Compose copy image

4.7 设置背景颜色

默认情况下,可组合项背景是透明的。要添加背景,请添加 showBackgroundbackgroundColor 参数。

注意: backgroundColor 是 ARGB Long 类型,而不是颜色值:

@Preview(showBackground = true, backgroundColor = 0xFF00FF00)
@Composable
fun WithGreenBackground() {
    Text("Hello Itmob.cn")
}

Jetpack Compose background color

4.8 设置尺寸

默认情况下,@Preview 的尺寸是自动选择来包裹其内容。如果要手动设置尺寸,可以添加 heightDpwidthDp 参数。

注意:这些值已被解释为 dp 无需在值末尾添加 .dp

@Preview(widthDp = 50, heightDp = 50)
@Composable
fun PreviewSquareComposable() {
    Box(Modifier.background(Color.Yellow)) {
        Text("Hello ITmob.cn")
    }
}

Jetpack Compose dimensions

4.9 区域设置

要测试不同的用户区域设置,需要添加 locale 参数:

@Preview(locale = "fr-rFR")
@Composable
fun DifferentLocaleComposablePreview() {
    Text(text = stringResource(R.string.greetings))
}

Jetpack Compose locale

4.10 显示系统界面

如果需要在预览中显示系统界面的状态栏和操作栏,请添加 showSystemUi 参数:

@Preview(showSystemUi = true)
@Composable
fun DecoratedComposablePreview() {
    Text("Hello ITmob.cn")
}

Jetpack Compose show system UI

4.11 界面模式

参数 uiMode 可以采用任何 Configuration.UI_* 常量,并允许您相应地更改预览的行为。

例如,您可以将预览设置为夜间模式以查看主题的效果。

@Preview(
    name = "dark theme",
    group = "themes",
    uiMode = UI_MODE_NIGHT_YES
)
@Composable
fun UiModePreview() {
    ComposeTextDemoTheme {
        Surface {
            Text("Hello Itmob.cn")
        }
    }
}

Jetpack Compose ui mode

5. @PreviewParameter 参数的用法和怎样支持大的示例数据集合

通常,需要将一个大数据集传递给可组合预览。为此,只需通过添加带有 @PreviewParameter 注解的参数,将示例数据传递给 Composable Preview 函数。

@Preview
@Composable
fun UserProfilePreview(
    @PreviewParameter(UserPreviewParameterProvider::class) user: User
) {
    ElevatedCard(shape = ShapeDefaults.Small) {
        Text(
            modifier = Modifier.padding(8.dp),
            text = user.name
        )
    }
}

要提供示例数据,需要创建一个实现 PreviewParameterProvider 的类并将示例数据作为序列返回。

class UserPreviewParameterProvider : PreviewParameterProvider<User> {
    override val values = sequenceOf(
        User("Elise"),
        User("Frank"),
        User("Julia")
    )
}

data class User(
    val name: String
)

Android Studio 会为序列中的每个数据元素呈现一个预览:

Jetpack Compose preview parameter

多个预览可以使用相同的 Provider。如有必要,可通过设置 limit 参数来限制预览次数。

@Preview
@Composable
fun UserProfilePreview(
    @PreviewParameter(UserPreviewParameterProvider::class, limit = 2) user: User
) {
    UserProfile(user)
}

6. 总结

@Preview 只可以应用于以下两种情况:

  • 不带参数的可组合函数,可以在 AndroidStudio 预览中显示。
  • 用于注解可组合函数的或其他注解类的注解类,这些注解类视为 Preview 的间接注解。

@Preview 注解的可组合函数不能带有参数(使用 @PreviewParameter 提供数据集合除外)

6.1 错误提示:Multiple @PreviewParameter are not allowed.

预览可组合函数只允许提供一个 @PreviewParameter 参数。多个参数无法渲染。

Mutiple @PreviewParameter ar not allowed

6.2 异常:IllegalArgumentException

如果 @Preview 注解的可组合函数有参数,Android Studio 无法完成渲染,会在渲染错误面板出现如下错误信息:

java.lang.IllegalArgumentException: argument type mismatch
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at androidx.compose.ui.tooling.ComposableInvoker.invokeComposableMethod(ComposableInvoker.kt:155)
	at androidx.compose.ui.tooling.ComposableInvoker.invokeComposable(ComposableInvoker.kt:195)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1$composable$1.invoke(ComposeViewAdapter.kt:711)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1$composable$1.invoke(ComposeViewAdapter.kt:709)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1.invoke(ComposeViewAdapter.kt:746)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1.invoke(ComposeViewAdapter.kt:704)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.tooling.InspectableKt.Inspectable(Inspectable.kt:61)
	at androidx.compose.ui.tooling.ComposeViewAdapter$WrapPreview$1.invoke(ComposeViewAdapter.kt:651)
	at androidx.compose.ui.tooling.ComposeViewAdapter$WrapPreview$1.invoke(ComposeViewAdapter.kt:650)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.tooling.ComposeViewAdapter.WrapPreview(ComposeViewAdapter.kt:645)
	at androidx.compose.ui.tooling.ComposeViewAdapter.access$WrapPreview(ComposeViewAdapter.kt:135)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3.invoke(ComposeViewAdapter.kt:704)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3.invoke(ComposeViewAdapter.kt:701)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:404)
...

6.3 错误提示:non-default parameters are not supported in Preview

网上其他文章提到 @Preview 注解的可组合函数支持 modifier 参数。

直接说我验证的结果吧:

@Preview 注释的可组合函数可以添加 modifier 参数,但必须是只有 modifier 参数。如果可组合函数有 @PreviewParameter 注释的参数,modifier 参数有没有默认值 Android Studio 都会报错无法渲染。

下文是 @Preview 增加 modifier 参数可能的错误信息,不关心可以直接跳过

@Preview
@Composable
fun UserProfilePreview(
    modifier: Modifier,
    @PreviewParameter(UserPreviewParameterProvider::class) user: User
) {
    Text(
        modifier = Modifier.padding(8.dp),
        text = user.name
    )
}

如果设置 Modifier 参数但不提供默认值 Android Studio 会显示如下错误提示:

Composable functions with non-default parameters are not supported in Preview unless they are annotated with @PreviewParameter.

渲染错误面板也有错误信息:

java.lang.NullPointerException: Parameter specified as non-null is null: method cn.itmob.sample.PreviewSampleKt.UserProfilePreview, parameter modifier
   at cn.itmob.sample.PreviewSampleKt.UserProfilePreview(PreviewSample.kt:-1)
   at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)
   at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
   at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
   at java.lang.reflect.Method.invoke(Method.java:566)
   at androidx.compose.ui.tooling.ComposableInvoker.invokeComposableMethod(ComposableInvoker.kt:155)
   at androidx.compose.ui.tooling.ComposableInvoker.invokeComposable(ComposableInvoker.kt:195)
   at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1$composable$1.invoke(ComposeViewAdapter.kt:711)
   at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1$composable$1.invoke(ComposeViewAdapter.kt:709)
   at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1.invoke(ComposeViewAdapter.kt:746)
   at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1.invoke(ComposeViewAdapter.kt:704)
   at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
   at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
   at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
   at androidx.compose.ui.tooling.InspectableKt.Inspectable(Inspectable.kt:61)