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
注释传递参数来自定义预览。例如,默认情况下,预览的标签是根据预览函数的名称而定,如上面例子中为 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")
}
@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 将预览部署到设备或模拟器
如需将预览部署到设备或模拟器,请点击 部署 按钮。Android Studio 将创建一个包含该函数生成的界面的新 Activity,并将其部署到设备上您的应用中。这样无需重新安装整个应用并导航至其该界面所在位置,即可在实际设备上试用该界面。
4.2 互动式预览
Android Studio 提供了互动式预览模式。当处于互动式预览模式下时,可以点击或输入界面元素,而界面会像在已安装的应用中那样进行响应。如需打开互动式预览,请点击任意预览窗口上的 互动式 按钮。预览面板将切换为该预览函数的互动模式,直到您退出该模式为止。
例如,响应用户点击的函数:
@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
}
)
}
- 这个示例来自官方文档,但使用 Material 3 做了修改
4.3 本地检查模式(LocalInspectionMode)
可以从 CompositionLocal
类型的 LocalInspectionMode
查看可组合项是否是在预览中呈现(在可检查组件内)。如果是在预览中呈现,则 LocalInspectionMode.current
为 true
。
这允许您自定义预览;例如,您可以在预览窗口中显示占位符图像而不是显示真实数据。
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")
}
多个多重预览注解和普通预览注解可以组合使用,以创建一组更完整的预览。组合多重预览注解并不是着显示它们所有不同的组合。而是每个多重预览注释都独立运行,并且仅呈现其自己的变体。
示例:
// 三种设备的多重预览
@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")
}
}
}
正如上文提到的多个多重预览和普通预览组合使用时,它们不会像 Gradle 多渠道打包(product flavors)那样
flavorDimensions
和productFlavors
会组合成不同的组合,而是各个多重预览和普通预览独立运行(如:效果图中预览视图呈现了6个预览:dark theme、三个不同设备的多重预览、两个不同字体缩放的多重预览)。
4.5 代码导航和可组合项轮廓
将鼠标悬停在预览视图上可以查看其中包含的可组合项的轮廓。单击可组合项的轮廓会触发编辑器视图导航到定义它的代码。
4.6 复制@Preview渲染
每个可组合项渲染的预览都可以通过 右键 - Copy Image 来复制渲染后的图像。
4.7 设置背景颜色
默认情况下,可组合项背景是透明的。要添加背景,请添加 showBackground
和 backgroundColor
参数。
注意: backgroundColor
是 ARGB Long
类型,而不是颜色值:
@Preview(showBackground = true, backgroundColor = 0xFF00FF00)
@Composable
fun WithGreenBackground() {
Text("Hello Itmob.cn")
}
4.8 设置尺寸
默认情况下,@Preview 的尺寸是自动选择来包裹其内容。如果要手动设置尺寸,可以添加 heightDp
和 widthDp
参数。
注意:这些值已被解释为 dp 无需在值末尾添加 .dp
@Preview(widthDp = 50, heightDp = 50)
@Composable
fun PreviewSquareComposable() {
Box(Modifier.background(Color.Yellow)) {
Text("Hello ITmob.cn")
}
}
4.9 区域设置
要测试不同的用户区域设置,需要添加 locale
参数:
@Preview(locale = "fr-rFR")
@Composable
fun DifferentLocaleComposablePreview() {
Text(text = stringResource(R.string.greetings))
}
4.10 显示系统界面
如果需要在预览中显示系统界面的状态栏和操作栏,请添加 showSystemUi
参数:
@Preview(showSystemUi = true)
@Composable
fun DecoratedComposablePreview() {
Text("Hello ITmob.cn")
}
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")
}
}
}
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 会为序列中的每个数据元素呈现一个预览:
多个预览可以使用相同的 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
参数。多个参数无法渲染。
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)