
看到“Compose 预览”这个需求先别急着往 Docker Compose 上想在 Android 开发这个语境里它指的是 Jetpack Compose 的 UI 预览能力。简单说就是不用跑模拟器、不用连真机直接在 Android Studio 里把 Composable 界面“画”出来。这个功能看起来只是省了安装 App 的时间实际上它是把 UI 开发的反馈循环从“分钟级”压缩到了“秒级”。我用了两年多 Compose从当初觉得预览是鸡肋到现在完全离不开它中间踩了不少坑。这篇就把 Compose 预览的原理、参数、进阶用法和排查经验一次性讲清楚。1. 先搞清楚 Compose 预览是什么它到底在渲染什么1.1 它和 XML 布局预览不是一回事很多人第一次接触 Compose 预览会下意识拿它和以前 XML 布局的 Design 视图对比。我一开始也这么想但用一个星期后就发现这俩的底层逻辑完全不同。XML 的布局预览本质是 IDE 对一个静态布局文件做解析然后把 View 层级“画”出来。它不执行你的 Java/Kotlin 代码所以很多动态逻辑、自定义 View、数据绑定效果根本看不到最后还是要跑模拟器验证。Compose 预览完全不是这样。它会在 Android Studio 的渲染环境里真正调用你的 Composable 函数。你写在函数里的条件判断、状态读取、列表拼接、主题切换都会被执行一遍。这带来的好处很直接一个组件在不同数据下的表现你不再需要造一堆页面去验证写几个预览函数就能看个八九不离十。当然代价也比 XML 预览高。因为它是执行代码而不是解析静态文件所以一旦 Compose 代码里引用了不适合预览环境的东西比如网络请求、系统服务、复杂生命周期逻辑预览就可能空白或者报错。理解这一点是后面所有排查思路的基础。1.2 一秒刷新背后发生了什么我最初很好奇一个问题为什么同一个 Composable在模拟器上跑可能卡顿但在预览面板里却能快速渲染甚至秒开后来看了 Compose Tooling 的实现思路才明白Android Studio 的做法是在编译阶段扫描所有带Preview注解的函数收集注解里的参数名称、设备、宽高、主题等生成一份预览描述信息。然后 IDE 里的预览渲染进程根据这些描述去构造一个脱离完整 App 的“最小执行环境”。注意这个前提它不是在跑你的 Application也不是在跑一个完整 Activity。它只实例化被标记的 Composable以及这个 Composable 直接或间接依赖的对象。所以你的MainActivity里写了什么启动逻辑你的 Application 里做了什么初始化预览时统统不执行。这也是为什么预览能快但也为什么预览会和你真机效果不完全一致。它跳过了很多 Android 运行时环境里的东西比如真正的 Context、资源解析、系统服务、动态权限等等。把这一点记在心里后面遇到预览和真机不一致的怪问题很多都不难解释。1.3 哪些场景适合用预览哪些别硬用我自己判断预览值不值得用的标准就一句话这个界面的显示是否主要取决于传入的数据和主题。适合预览的场景我列几个典型的组件库里的通用组件比如按钮、标签、卡片、输入框换不同文案和状态验证布局。页面骨架比如商品列表、个人中心、设置页面的静态结构。主题适配同一套页面在浅色、深色、大字体下的表现。多尺寸适配同一组件在不同屏幕宽度下是否挤压或溢出。不适合硬用预览的场景也很明确依赖真实网络返回数据的页面、依赖 ViewModel 并且 ViewModel 里有复杂初始化逻辑的页面、需要摄像头/传感器等硬件的功能、以及重度依赖协程和生命周期的动画流程。这些不是不能预览而是预览的成本会高于直接跑模拟器收益不明显。2. 搭出一个能稳定预览的工程环境2.1 Gradle 依赖和编译选项先说环境。预览功能对工程配置是有要求的不是随便新建一个 Compose 工程就万事大吉。在模块的build.gradle.kts里首先要开启 Compose 编译android { buildFeatures { compose true } }然后加上必要的依赖。我的建议是别手动写一堆版本号直接用 Compose BOM 统一管理省得依赖版本不一致导致预览异常dependencies { implementation(platform(androidx.compose:compose-bom:2024.09.00)) implementation(androidx.compose.ui:ui) implementation(androidx.compose.ui:ui-graphics) implementation(androidx.compose.ui:ui-tooling-preview) debugImplementation(androidx.compose.ui:ui-tooling) }这里有一个容易踩的坑ui-tooling-preview是负责让 IDE 识别Preview注解的依赖很多老项目里可能会漏掉它。如果依赖缺失代码里写Preview不会报错但预览面板就是不出现或者一直显示“No Compose Previews”。另外如果你用的是比较老的 Kotlin Compose Compiler 插件方案不是 Kotlin 2.x 内置的 Compose 编译器还要保证 Kotlin 插件和 Compose Compiler 插件的版本匹配否则 IDE 解析注解阶段就可能直接编译失败。现在 Kotlin 2.0 之后一般不需要单独配 Compose Compiler 插件了但升级时要留意项目里的历史配置。2.2 写出第一个可用预览新建一个 Compose 工程之后默认模板里一般会有一个GreetingPreview。如果工程是自己手动搭的按下面这个最小结构写就够了Preview( name 默认浅色, group 基本预览, showBackground true ) Composable fun GreetingPreview() { AppTheme { Greeting(name Android) } }注意几个细节预览函数的命名不要随便起name参数会直接显示在预览面板的下拉列表里。团队协作时明确的命名比“预览 1”“预览 2”要省太多沟通成本。预览函数内部一定要包主题。很多人直接写“裸组件”导致预览里字体、颜色和实际 App 完全不一样。因为预览环境不会自动帮你套 App 的主题你必须显式调用主题 Composable。预览函数最好不要有普通参数。除了后面会讲到的PreviewParameter其他情况下预览函数最好是无参的。如果预览函数本身有参数IDE 没法自动填值预览面板就会报错。2.3 多个预览的组织方式一个文件里写很多Preview函数是完全正常的但我更推荐按功能块组织而不是把整个页面所有组件的预览都堆在一个文件里。原因是预览函数也是要编译的文件太大IDE 预览渲染时反复刷新会很吃力。我习惯的做法是组件放在组件同名文件里预览页面级预览单独放。比如UserCard.kt里写UserCard和对应的UserCardPreview而HomeScreen.kt的页面预览因为涉及较多依赖单独放一个HomeScreenPreviews.kt方便后续删除或维护。预览面板的展示顺序是由group参数控制的。同一个组里的预览会在下拉列表里聚在一起而不是按函数名乱序排列。组件多的时候这个分组功能会非常香。3. Preview 参数拆解这才是“完整”的关键3.1 最常用的参数速查表Preview注解说白了就是一个配置入口。它本身不做事但它提供的参数会直接告诉 IDE 怎么渲染。我整理了一张常用参数表供现查现用参数作用常用值示例name预览显示名称name 深色模式group预览分组group 主题适配showBackground是否显示背景画布showBackground truebackgroundColor设置预览背景颜色backgroundColor 0xFFFFFFFFshowSystemUi是否显示状态栏和导航栏showSystemUi trueuiMode模拟系统 UI 模式夜间等uiMode Configuration.UI_MODE_NIGHT_YESwidthDp指定预览宽度dpwidthDp 360heightDp指定预览高度dpheightDp 640fontScale模拟系统字体缩放比例fontScale 1.5flocale模拟本地语言区域locale zh-rCNdevice指定预览设备device id:pixel_4apiLevel指定模拟 API 级别apiLevel 33粗看下来你会觉得这不就是一堆面板选项吗但真正决定预览质量的是组合方式。单独用showBackground true和同时配backgroundColor效果完全不同。showBackground会在预览层后面画一块画布而backgroundColor才是画布本身用的颜色。如果你在深色主题下预览一个深色卡片却不设置backgroundColor画布还是白色对比起来会非常刺眼。3.2 组合参数的几种经典姿势实际开发里我经常同时用uiMode和widthDp。比如验证暗色模式在窄屏上的表现Preview( name 暗色窄屏, group 主题适配, showBackground true, uiMode Configuration.UI_MODE_NIGHT_YES, widthDp 360, device id:pixel_4 ) Composable fun DarkNarrowPreview() { AppTheme { UserCard( nickname 林一, description 这是一段用于验证折行和间距的说明文字 ) } }为什么我总爱加widthDp因为在不指定时预览面板默认是 IDE 给你选的一个设备尺寸。这个尺寸和你实际项目的适配目标可能并不一致。比如你手机是 360dp但预览面板默认给了一个 411dp 的宽度很多隐藏的换行问题、溢出问题就看不出来。我建议在写页面级预览时至少固定widthDp把常见宽度 320、360、411、480 都测一遍。不用一台台真机去借预览里几个函数就能覆盖大部分场景。3.3 关于 device 和 showSystemUi 的几个记忆点device参数能模拟具体设备但它的值是 IDE 里的设备 ID不同 Android Studio 版本可能不一样。想精确匹配某台真机最稳的方法不是背 ID而是先看 Android Studio 预览面板顶部的设备下拉框它列出的就是当前可用的设备值代码里可以直接套用下拉框展示的那个 ID。showSystemUi true这个参数我建议按需使用不要一开始就打开。它会把状态栏、导航栏画出来让预览更接近真机但也会占用视觉空间干扰你对内容布局的判断。只有当你确实要验证“沉浸式状态栏”“适配刘海屏”这类问题时再打开它。不然每次预览都带着系统栏改间距时很容易看走眼。4. 让预览真正有用的进阶玩法4.1 用 PreviewParameter 一次性看多个状态写 UI 最头疼的就是状态分支。一个用户卡片要覆盖有头像、无头像、名字特别长、加载中、加载失败这些状态。如果每改一个状态都要跑模拟器整个人会崩溃。这时PreviewParameter就是救星。它可以给预览函数注入一组不同的参数IDE 会在预览面板里按顺序渲染多个实例一眼看完整组状态。一个简单的例子data class UserUiState( val isLoading: Boolean false, val nickname: String , val avatarUrl: String ) class UserStateProvider : PreviewParameterProviderUserUiState { override val values sequenceOf( UserUiState(isLoading true), UserUiState(nickname 林一, avatarUrl https://example.com/avatar.png), UserUiState(nickname 这个用户的名字特别特别长需要检验折行效果, avatarUrl ), UserUiState(nickname 林一, avatarUrl , isLoading false) ) } Preview(showBackground true) Composable fun UserCardPreview( PreviewParameter(UserStateProvider::class) state: UserUiState ) { UserCard(state) }注意PreviewParameterProvider的实现类必须有一个公开无参构造IDE 才能通过反射实例化它。还有values里不要依赖外部 Context 或网络请求因为预览环境里这些都不可靠。我见过有人在这个 sequence 里直接访问BuildConfig或者用Context拿资源结果预览面板一片红。4.2 交互式预览怎么开静态预览只能“看”不能“点”。如果你需要验证点击、滑动手势、文本输入可以切换到互动模式Interactive Preview。开启方式很简单写好Preview之后在 Code Editor 右上角找到 Compose Preview 面板把渲染模式从默认的静态模式切到 Interactive。切过去之后光标会变成一个手型你就能在预览画布上点击按钮、输入文本、滑动列表了。我实际用下来互动模式应对大部分简单交互是足够的。但它的本质仍然是预览环境不是完整 App。像 ViewModel 里的状态变化、依赖 Hilt 注入的对象、从网络拉数据后再渲染这些流程互动模式往往不太灵。最安全的方式是把界面状态都提升为 Composable 的参数让预览函数直接传不同状态进去而不是期望互动模式“跑通整个业务”。4.3 主题、暗色和字体缩放一起验证我一直觉得只把浅色模式调好了不算调好了。深色模式、大字体模式这些如果靠真机一个个切换看效率太低。用预览把这些组合一口气铺开Preview( name 深色模式, group 主题适配, showBackground true, uiMode Configuration.UI_MODE_NIGHT_YES ) Composable fun DarkModePreview() { AppTheme { ProfileScreen() } } Preview( name 大字体模式, group 主题适配, showBackground true, fontScale 1.5f ) Composable fun LargeFontPreview() { AppTheme { ProfileScreen() } }uiMode Configuration.UI_MODE_NIGHT_YES会强制预览环境按深色配置运行这时候isSystemInDarkTheme()的判断也能跟着生效。fontScale 1.5f则是模拟系统把字体调大之后布局是否还能撑住。一个很容易忽略的细节是如果项目里用了自定义字体预览环境不一定能到 APK 里找字体文件。字体显示为方块的时候优先检查是不是预览环境资源解析的问题而不是马上怀疑代码。5. 常见问题与排查技巧5.1 预览一直转圈或空白这是我被问得最多的问题。第一步不是改代码而是先看右下角的 Render Problem 面板里面通常有具体错误信息。常见原因有三类第一代码编译没过。预览渲染前必须先编译编译失败时预览肯定出不来。这种情况先去 Build 面板看编译日志别盯着预览面板发呆。第二预览函数内部调用了不安全的依赖。比如直接在 Composable 里写了一堆业务逻辑访问了系统服务、数据库、网络库预览环境一执行到那块就崩。处理办法是把内容提取成可传参的状态用PreviewParameter喂干净数据。第三预览缓存出现了问题。有时候代码改了很多次预览还是显示旧内容这种往往是 IDE 的预览缓存和编译结果不同步。先点预览面板右上角的刷新按钮再不行就Build Clean Project还不行就File Invalidate Caches。这个顺序是从轻到重别一上来就清缓存。5.2 字体、图标和图片资源不显示预览环境对资源的处理比较特殊。painterResource(R.drawable.xxx)这种方式在大多数情况下是能正常预览的但如果你依赖的是运行时动态生成的图片、网络加载的图片、或者通过第三方图片库在 Composable 内部发起的异步加载预览里大概率是空白。一个很实用的替代方案是把图片显示抽成参数传入。组件只管画“传入的 Painter”预览时传一个本地占位图真机运行时传网络加载结果。这样既保证预览可见也不影响真实逻辑。5.3 预览和真机颜色不一致我经常看到有人预览里颜色很正跑到真机上偏色。绝大多数情况不是代码 bug而是预览面板和真机的色彩空间、屏幕色域不一样。尤其是 OLED 屏幕下同一段绿色肉眼看起来会差很多。我能给出的经验是预览的作用是验证“布局是否正确”而不是“颜色是否绝对一致”。颜色校准要依赖真机或者专业色卡不要在预览里盯着一个色值反复纠结。5.4 预览太多导致 IDE 卡顿项目变大以后如果每个小组件都写两三个预览整个模块的预览数量可能上百。这些预览在编译阶段都会被扫描处理IDE 打开预览面板时也可能同时渲染多个卡顿几乎是必然的。我的做法是给预览分组在面板里只渲染当前组。另外日常开发中只保留“正在验证的那一个”预览函数等组件稳定之后再删掉。不要觉得预览函数写多了很专业最终交付的代码里保持精简比展示“我写过很多预览”更有价值。5.5 我踩过的几个实际坑第一个坑是PreviewParameterProvider里用了有参构造。当时我为图方便在 Provider 里传了一个数据仓库对象结果 IDE 反射构造时直接抛异常。后来查资料才知道预览注解的 Provider 是无参实例化的构造参数会被忽略运行时直接崩。这个教训让我记住了预览环境是“独立的小世界”你的依赖注入方案在这里不自动生效。第二个坑是把showSystemUi true当默认配置。有一段时间我的预览全是“缩小的页面”因为系统栏吃掉了一部分高度。我以为页面有问题调了很久参数最后才发现只是预览配置导致的显示比例问题。从那以后这个参数我只在需要验证系统栏相关场景时才开。第三个坑比较隐蔽。预览函数内部调了LocalContext.current平时没问题但有一个页面通过 Context 去拿 SharedPreferences 做判断结果预览环境里拿到的 Context 不是 Activity Context行为很怪。虽然不报错但显示状态和真机完全不一样。遇到这种把数据先传入 Composable不要在预览路径里去依赖 Context。6. 最后分享一点个人习惯预览功能用到现在我最深的体会是它不是一个“看效果”的工具而是一个“逼你写出更纯函数式 UI”的工具。当你发现某个界面怎么预览都不对往往不是预览的问题而是你的 UI 和业务逻辑耦合得太深了。我现在的习惯是写组件之前先写预览函数用预览函数来“驱动”组件设计。组件需要什么参数、需要什么状态、需要什么主题在预览里一目了然。改布局时我会把常见的输入状态都放进 PreviewParameter确认无误再切真机做最终验证。最后再补一个小技巧如果预览面板偶尔不刷新先别急着清缓存试试把鼠标光标移到预览画布上按一下刷新快捷键大多数情况下只是渲染进程懒了一下。这和使用模拟器偶尔无响应一样属于正常现象不用过度紧张。