ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Jetpack Compose成熟度评估与落地实践:状态管理、重组与性能优化

Jetpack Compose成熟度评估与落地实践:状态管理、重组与性能优化 1. 现状观察Compose到底发展到哪一步了聊到Android Compose圈里人的态度大致可以分成两拨一拨觉得“这就是Android的未来赶紧梭哈”另一拨还在观望担心迁移成本高、性能兜不住底。我在一线写了十来年Android从View体系一路用到Compose这几年也带过几个完全用Compose从零搭建的项目也做过老项目渐进式混改。今天这篇不说那些官方文档里抄得到的话就从一个落地的角度聊聊Compose在当前这个时间点到底成不成熟哪些场景可以放心用哪些地方还得留个心眼。先说个结论截止到目前Compose在纯新项目里已经完全具备上生产的能力在老项目渐进式接入里也基本可行但前提是你得接受它那套完全不同于View体系的思维方式并且团队里至少有一个人能搞定Compose的底层原理——尤其是重组机制和生命周期。如果你拿Compose当“XML换了个写法”那大概率会踩坑踩到怀疑人生。先看一下Compose现在所处的位置。Jetpack Compose从2021年7月发布1.0版本到现在已经迭代到了1.5.x甚至1.6.xAPI已经基本稳定。Google自家的应用——比如Play Store、Maps、Home Control这些——都已经在生产环境里大规模用了。这个信号其实比任何技术博客都有说服力连Google都在自己最核心的产品里用Compose说明它已经过了“实验品”的阶段。但“基本稳定”不等于“处处顺手”。我见过不少团队用了Compose大半年之后又改回View体系的原因无外乎是几个性能瓶颈暴露在复杂列表上、团队学习成本太高、三方库兼容性滞后、以及Compose那套状态管理在多人协作时特别容易写出烂代码。这些坑我都踩过后文逐个说顺便把解法也聊透。1.1 一个技术到底算不算成熟得看这四件事判断一个UI框架是否成熟我自己的标准有四个维度不吹不黑挨个拿出来看Compose的表现。第一是API的稳定性。API三天两头变谁都不敢在它上面盖楼。Compose在1.0之后大部分核心API都稳定下来了但一些高频API还是有变动比如Modifier的排列顺序、LazyColumn的item key策略、以及rememberCoroutineScope的使用场景在1.x版本之间有过调整。我的建议是锁版本别追新。项目里明确固定Compose版本升级时要单独排期做回归测试不要跟着Android Studio的提示顺手就点了升级。第二是工具链的完善度。开发一个UI框架光有框架本身还不够调试工具、预览工具、性能分析工具都得跟上。Android Studio对Compose的支持从北极狐版本开始逐步完善到现在已经相当好用了——布局检查器能看到重组次数和跳过次数Compose Preview也支持交互模式。但说实话和View体系的成熟工具链比Compose的调试体验还是差那么一点尤其是遇到重组逻辑异常时定位问题比View体系难不少后面我会专门讲这个。第三是生态的适配度。这个维度Compose处于一个“基本能用偶有缺口”的状态。主流的图片库Coil、Glide都有Compose适配网络库Retrofit、OkHttp本来就和UI无关无所谓适不适配。但一些偏门的三方SDK——比如地图、直播、图表类的——在Compose里的支持就参差不齐了经常得用AndroidView去包一层传统的View实现。这种“包一层”的写法短期内能解决问题但会破坏Compose统一的声明式风格而且埋下性能隐患。第四是社区经验的沉淀。这个维度Compose已经积累了足够多的坑和解决方案国内外博客随手一搜都是。但从另一个角度看社区里也存在大量“半懂不懂”的文章把State、Flow、LaunchedEffect这些东西讲得云里雾里反而误导了一批初学者。这也是我觉得有必要写这篇文章的原因——有些东西得把原理说透了你才不会被那些“看似讲得很深、实则没说到点子上”的文章带偏。2. 生态与兼容性的真实战况2.1 兼容性Compose能不能无缝接进老项目能但有条件很多团队最关心的问题是现有的View体系代码要不要推倒重来答案很明确不需要也不建议。Compose和View体系不是二选一而是可以共存的。Google在设计Compose时就考虑到了渐进式迁移的场景所以提供了ComposeView这个容器——在XML布局里放一个ComposeView就可以把Compose UI嵌入到传统View体系中。反过来在Compose的代码里也可以通过AndroidView把传统View嵌进来。这种双向兼容的能力是Compose成熟度里最被低估的一个点。我在实际项目里用的混合方案是这样的新写的页面优先用Compose老页面保持View体系不动只有当某个老页面需要大改时才顺手迁移成Compose。比如去年我接手的一个电商项目购物车页面原本是RecyclerView多类型item的老写法改动需求频繁、代码越来越难维护我直接重构成了Compose LazyColumn效果立竿见影。而像订单详情这种改动少的页面就没必要动它留着View体系也没任何问题。具体操作上有一个注意点Compose和View体系间的数据流转尽量用ViewModel来承载。Compose里不要直接持有Android View的引用View体系里也不要直接读Compose的State两边各写各的通过ViewModel统一通信。这样混改期间架构最干净后面想继续迁移也容易。2.2AndroidView包裹三方SDK的几个坑说个实际的。我们项目里用过一个第三方的直播SDK它只提供传统View的接口比如一个叫LivePlayerView的类继承自FrameLayout。在Compose里要集成它标准做法就是套一层AndroidViewComposable fun LivePlayerScreen(url: String) { val playerView remember { LivePlayerView(context).apply { setUrl(url) startPlay() } } AndroidView( factory { playerView }, modifier Modifier.fillMaxSize() ) }看起来很简单对吧但实际用起来有几个坑第一个坑是生命周期绑定。AndroidView里的View不会自动感知Compose的生命周期你得自己在DisposableEffect里手动管理Composable fun LivePlayerScreen(url: String) { val context LocalContext.current val playerView remember { LivePlayerView(context) } DisposableEffect(Unit) { playerView.setUrl(url) playerView.startPlay() onDispose { playerView.stopPlay() playerView.release() } } AndroidView( factory { playerView }, modifier Modifier.fillMaxSize() ) }不写这个DisposableEffect的后果是页面退出后播放器还在跑声音还在响内存也释放不了。这是Compose混用传统View时最容易翻车的点。第二个坑是更新时机。传统View经常有类似setData(data: ListItem)的方法在View体系里调用很自然。但放到Compose里你不能在重组时直接调用它因为重组会跳过或者重复执行导致数据被重复设置。正确做法是把这类调用放进LaunchedEffect或者SideEffect里根据key的变化来触发更新Composable fun LivePlayerScreen(url: String) { val playerView remember { LivePlayerView(LocalContext.current) } LaunchedEffect(url) { playerView.setUrl(url) playerView.startPlay() } AndroidView( factory { playerView }, modifier Modifier.fillMaxSize() ) }2.3 团队切Compose的学习曲线问题技术选型不只是技术问题更是团队管理问题。Compose的编程范式和View体系差异极大从“命令式”到“声明式”的转变这个坎不是所有人都能轻松迈过去的。我在内部推Compose时统计过团队的学习成本一个熟悉Android但没接触过声明式UI的工程师大约需要两到三周才能独立写页面一个多月才能写出符合Compose惯用法的代码。这里说的“符合惯用法”不是你实现了功能就行而是你的状态管理、重组设计、Modifier使用都是规范的不会写出那种“能跑但性能稀烂”的代码。最容易让新手翻车的地方有几个。一个是状态提升该提升的不提升导致状态存在某个无关的Composable里一改数据整个页面重组一个是remember和rememberSaveable的滥用为了省事全部无脑套remember结果数据存不住、状态错乱还有就是不理解LazyColumn的重组机制把所有item都写成一个巨大的Composable导致滑动时疯狂重组、帧率直线下降。我的建议是团队推Compose之前先找一两个人做技术预研把常用的组件库封装好沉淀出一套内部的Compose风格规范再铺开给所有人。不要一上来就让全员直接写Compose——没人带的情况下三个月后会写出一堆没法维护的代码然后你就会听到“Compose只是个玩具”之类的结论。3. 性能账本重组与监听的“军备竞赛”3.1 Compose的三大心脏State、重组与remember想用好Compose先得搞清楚它最底层的运行机制。Compose的核心是“声明式UI”你描述的是UI在不同状态下的样子状态一变化UI自动更新。这个“自动更新”的底层就是重组——Compose会重新执行发生过变化的Composable函数生成新的UI。但这里有个关键概念重组不是全量重跑而是“跳过没变的只重跑那个变了的”。Compose通过“位置记忆”来跟踪每个Composable的状态如果某个Composable读取的状态没有变它就不会重新执行。这个机制设计得很好但如果你的代码不规范它就会退化成“页面一抖全量重组”性能直接崩盘。remember的作用是在重组时保留数据避免初始化和计算逻辑重复执行。举个例子Composable fun ExpensiveList() { val data remember { loadExpensiveData() } // 只会在首次进入时加载 LazyColumn { items(data) { item - Text(item.title) } } }如果去掉那个remember每次重组都会重新调用loadExpensiveData()用户滑个列表转场调个色都会触发这个昂贵的加载逻辑。这属于最基础的优化但越是基础越容易犯。3.2 “逆行依赖”与“定向重组”为什么我用Compose重构后更卡了很多团队从View切到Compose后第一个直观感受就是怎么滑动列表比以前卡了我以前也困惑过这个问题后来仔细排查才发现原因往往出在“状态放错了位置”。来看一个反面例子。这是一个常见的错误写法Composable fun MessageList(messages: ListMessage) { Column { var expanded by remember { mutableStateOf(false) } MessageListItem(messages, expanded) // 注意expanded传给子节点 } }当你切换expanded时理论上只有当前页面需要重组。但因为MessageList读取了expanded状态而messages也是它的参数导致整个列表——包括所有item——全部重新组合了一遍。这在列表较长时会非常明显切换一个展开动画整个列表都跟着闪一下。正确的做法是“状态下沉”——让状态只存在于需要它改变的最小范围内。比如这个例子应该把expanded放进MessageListItem内部或者直接把状态声明在item的那一层Composable fun MessageList(messages: ListMessage) { LazyColumn { items(messages, key { it.id }) { message - MessageListItem(message) } } } Composable fun MessageListItem(message: Message) { var expanded by remember { mutableStateOf(false) } // 只影响当前item }在动手重构之前先做一次全项目的状态审计查清楚哪些状态放错了层级、哪些Composable被无谓地重复组合。跳过这一步你就连“为什么我的Compose那么卡”这个问题的答案都找不到。3.3 稳定性标记与Stable/Immutable进阶优化的必修课到了Compose性能优化的深水区就绕不开“稳定性”Stability这个概念。你可以把它理解成Compose编译器的一个判断这个参数在重组时到底变了没有如果编译器能确认你没变它就会跳过这次重组如果确认不了它会“宁可信其有不可信其无”——每次都重组保证不会出bug。这个判断机制很关键但默认情况下它比较保守。比如你定义了一个普通的数据类data class User(val name: String, val age: Int)编译器会认为它“可能没变”因为它无法确认这个类的属性在某次状态更新后是否保持原样。于是每次父级重组它都会把这个User重新传给子级Composable子级全部跟着重组。这在列表场景下会导致滑动时所有item都重组——明明数据没变UI却全部重画一遍。解决方案是加稳定性标记Immutable data class User(val name: String, val age: Int) // 或者 Stable data class User(val name: String, val age: Int) { var isFollowing by mutableStateOf(false) }Immutable告诉编译器这个类永远不会变Stable告诉编译器我有部分字段是稳定的你可以智能地跳过重组。这两个注解用好了复杂列表的性能会有质的提升。注意一个坑只有当你确定这个类的所有属性真的不变时才用Immutable否则会有隐藏bug——UI不更新你排查半天都找不到原因。稳妥起见用Stable比用Immutable更安全。3.4LazyColumn的key不写key滑动列表就会“花”LazyColumn是Compose里的列表组件对标View体系里的RecyclerView。RecyclerView有一个DiffUtil机制自动计算item差异而LazyColumn的差异计算能力则弱得多——它默认按位置对比不写key就会出现“数据错位”的诡异问题。举个我踩过的坑。当时做一个聊天列表item里有个“已读/未读”的状态Composable fun ChatList(messages: ListMessage) { LazyColumn { items(messages) { message - ChatItem(message) } } }当用户在某个item上触发“已读”操作时那个item的数据变了但因为没写keyLazyColumn按位置对比发现位置没变就不刷新UI。结果用户点了一下已读界面纹丝不动但没有key的新数据插入时又会导致整个列表错乱。正确写法是明确指定keyitems(messages, key { it.messageId }) { message - ChatItem(message) }有了key之后LazyColumn才能准确定位到“哪一条item变了”组合逻辑才能在正确的地方触发更新。这个key的选择也有讲究最好用业务上的唯一ID不要用index或者随机数。4. 状态管理与架构设计代码能不能撑住三年4.1 单向数据流别把State传得满天飞Compose的状态管理并不难难的是在多人协作的场景下保持一致。我在项目里看过最糟糕的Compose代码是一个页面上有十几个remember每个Composable都自己管理状态互相之间通过回调函数通信读代码的人完全分不清谁是谁的父节点、谁在什么时候改了什么数据。这种情况和View体系时代“Activity写了一坨集合了UI状态和业务逻辑”的“上帝类”本质上是同一个问题状态管理没有边界业务逻辑和UI逻辑混在一起。正确的Compose状态管理应该是这样的ViewModel里用StateFlow或State持有UI状态业务数据只管往ViewModel里扔UI层通过collectAsStateWithLifecycle()收集状态只做一件事把状态渲染成UI用户操作通过回调/事件传给ViewModelViewModel处理后更新状态代码看起来就是这样的结构class MyViewModel : ViewModel() { private val _uiState MutableStateFlow(MyUiState()) val uiState: StateFlowMyUiState _uiState.asStateFlow() fun onButtonClick() { _uiState.update { it.copy(count it.count 1) } } } Composable fun MyScreen(viewModel: MyViewModel viewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() MyContent( uiState uiState, onButtonClick viewModel::onButtonClick ) }这种单向数据流的好处是状态有且只有一个“真相来源”ViewModelUI只是状态的投影逻辑清晰测试也好写。团队里不管谁来看代码都能很快理清“数据从哪里来、要到哪里去”。4.2collectAsStateWithLifecycle()一个必须养成的习惯上面代码里我写的是collectAsStateWithLifecycle()这个细节值得单独说一句。在早期Compose版本里很多人用的是collectAsState()。两者最核心的区别在于collectAsState()不会感知生命周期就算App退到后台它也会继续收集Flow里的数据不仅浪费资源还可能在后台触发UI更新导致崩溃。collectAsStateWithLifecycle()是lifecycle-runtime-compose库提供的方法它只在STARTED及以上状态收集数据App进后台就自动停止收集回到前台再恢复。这个差异在做过性能优化的人眼中简直是天上地下。用它应该像用LaunchedEffect一样自然想都不用想。我当时在项目里全面替换掉collectAsState()之前电量和性能本来是困扰我们很久的问题替换后明显改善——这就是生命周期感知的重要性。4.3 延续View体系里的MVVM要不要引入Flow在Compose项目的架构选型上有一条经常被问起在Compose里用Flow合适还是用MutableState合适这两个我都用过说下我的体会。MutableState是Compose原生的状态容器最大的特点是和Compose的读写追踪无缝配合性能极好适合“轻量级、即时性”的状态比如用户的输入内容、下拉刷新标志等。但它不适合做跨页面共享的状态——你在两个页面间硬要传一个MutableState就约等于制造了一个隐式的全局变量代码一旦复杂起来就非常难看。Flow则更适合承载“异步、事件流、数据源”类的状态比如网络请求的结果、本地数据库的监听。它的操作符丰富可以做map、filter、debounce等操作这是MutableState做不到的。把它和StateFlow结合配合collectAsStateWithLifecycle()在ViewModel里作为“真相来源”是目前最主流的方案。我的个人习惯是页面内部、临时状态用MutableState跨页面共享、异步数据用Flow/StateFlow。两者不是二选一而是在不同场景下各司其职。4.4remember与rememberSaveable不要把“临时”和“持久”混为一谈有次和同事做Code Review看到他在一个登录表单页里用了remember来存用户输入的账号Composable fun LoginScreen() { var username by remember { mutableStateOf() } var password by remember { mutableStateOf() } // ... }这个写法的bug是当用户切到后台进程被系统回收或者屏幕旋转后账号密码就全丢了。用户可能已经填了半天的表单一个转向电话或者横竖屏切换全都白填了体验非常糟糕。如果还想维持合理的状态恢复体验就应该用rememberSaveable——它会把状态保存进Bundle里在Activity重建时恢复var username by rememberSaveable { mutableStateOf() } var password by rememberSaveable { mutableStateOf() }能用rememberSaveable的地方就尽量用它比remember多一个自动保存恢复的能力成本几乎为零却能在关键时刻救用户体验一把。当然rememberSaveable只能存Bundle支持的类型如果状态里有自定义对象要么实现Parcelable/Serializable要么用listSaver/mapSaver去转换。实在复杂的就别省事了放ViewModel里用SavedStateHandle。5. 工具链与生态横向对比Compose够不够“趁手”5.1 Android Studio的Compose支持多年的“亲儿子”待遇工具链是衡量一个框架成熟度的硬指标。Android Studio对Compose的支持从北极狐版本开始逐步加强到目前的海豚、Electric Eel等版本已经相当完善。布局预览是Compose开发里我最依赖的功能之一。View体系的XML预览只能静态展示Compose Preview则支持交互模式——你可以直接在预览里点击按钮、输入文字实打实地操作UI这在调试UI交互时能省下大量的构建和运行时间。多个不同状态的Preview用Preview注解配合参数就能展示一个列表的加载态、空态、有数据态可以并排放在一起看一眼就能发现布局问题。布局检查器是Compose调试的另一个杀手锏。在View体系时代想看一个View为什么高度不对、margin为什么没生效得一层层翻XML和代码。Compose的布局检查器里你能直接看到每个Composable的边界、尺寸、padding还能看到重组次数——那些“我明明没改数据为什么这里还在重组”的问题打开布局检查器一看就明白了。不过说实话Compose的工具链虽然“亲儿子”待遇拉满但仍然有不如View体系的地方。比如复杂动画的调试View体系有专门的动画预览工具Compose目前只能靠肉眼看和录屏慢放。还有Lint检查Compose的Lint规则比View体系少得多很多性能隐患得靠开发者自己经验去发现。5.2 编译期与运行时的优化“打包时”和“运行时”两把尺子量到底很多人担心Compose的编译时间和包体增量我也实测过。一个中等规模的项目大约50个页面使用Compose后冷启动的编译时间大约增加了15%到25%全量构建大约增加几十秒。增量构建的影响相对较小因为Compose编译器插件会做增量处理但第一次全量构建确实会让人多等一会儿。包体方面Compose及其相关的库会增加大约4MB到6MB的体积未压缩后大约9MB左右。如果你的App对包体特别敏感——比如有些应用商店限制包体大小——这个增量是需要认真考虑的。但对大多数App来说多几个MB换来的开发效率和可维护性我个人认为是值得的。运行时性能上Compose本身并不慢。它的底层用了一个叫“组合线程Compose Runtime”的机制把UI描述编译成一种高效的字节码指令执行效率并不比传统的View绘制差。在正确使用Stable/Immutable/remember的前提下Compose完全可以做到流畅的60fps甚至120fps。但**“正确使用”这四个字是门槛**。View体系下即使你写得再烂RecyclerView的ViewHolder机制也能兜住一部分性能底线。而在Compose里一个状态放错位置、一个Stable标记缺失就能让列表卡成PPT。它把性能优化的责任从框架转移到了开发者的手里——这既是好事上限更高也是坏事下限更低。5.3 与Flutter、RN的横向视角同为声明式手感不太一样因为项目经历过React Native、Flutter再加上现在的Compose我有机会横向比较几个主流的跨端/声明式框架说几点直观感受。Flutter的声明式做得非常彻底整个渲染引擎都是自己的性能一致性极好“写一遍到处跑”体验最好。但它最大的问题是“住在自己的岛上”——它和Android原生的交互没有Compose这么顺滑嵌入原生View或者被原生View嵌入都需要额外的channel桥接而且Dart语言本身也是团队学习的一个门槛。React Native这么多年了生态依然是最丰富的前端有大量成熟的库可以直接用。但在Android平台的性能一致性上RN它还需要通过Bridge现在是Fabric和原生通信复杂动画和高频操作下容易丢帧。而且RN写UI时仍然是“JS对象到原生组件”的映射而不是像Compose/Flutter那样把渲染的关键环节握在自己手里。Compose的好处在于它“生在Android活在Android”和Android生态的整合是最好的可以无缝调用任何原生API三方SDK用AndroidView包一下就行不需要额外的桥接层。坏处也很明显——它没法跨端iOS只能考虑Compose Multiplatform但据我之前看过的资料和实验Compose Multiplatform的成熟度目前还属于“能跑但别轻易上生产”的状态。所以我的判断如果你的项目是纯Android端Compose是长期最优解如果要跨端Flutter依然是更稳的选择RN则适合那种已经以Web技术栈为核心的团队。没有绝对的“最好”只有“当前条件下最合适”。5.4 我试过的Compose门下的性能瓶颈定位工具前面说过Compose的调试工具不如View体系成熟但这几年下来也积累了几个比较实用的定位手段分享出来供参考。手段一布局检查器看重组次数。Android Studio的Layout Inspector可以实时显示每个Composable的重组次数。如果发现某个列表项的Composable重组次数异常高比如每帧都在跳动那基本可以确定有状态被无谓地共享了。手段二Preview配合不同状态排查UI问题。以前在View体系里调UI遇到问题得改完代码重新build、跑起来、手动触发状态预览时还很容易遗漏异常状态。Compose Preview可以给同一个UI组件定义多组Preview参数直接并行预览正常、空、加载、错误等状态能在编码阶段就发现很多布局问题。手段三开启Compose编译器的“强模式”。在Compose编译器的gradle配置里有个strongSkippingMode不同版本名称略有差异开启后编译器会做更多的跳过优化这个模式下某些性能问题会自动消失。但它不是银弹而且用了它后有些原本依赖“人类惰性写法”的代码会被编译器当“不稳定”处理导致更多重组所以开启前要评估好。手段四二分法和注释法。这是最笨但最有效的方式。当遇到“某个界面滑动卡顿”的问题可以先把页面里的组件逐个注释掉跑一遍确认卡顿是否消失然后逐步缩小范围。性能问题往往不是由一个点引起的但“定位到一个最可疑的点”比“一次全排查一遍”效率高得多。这个方法论在所有UI框架里都适用。6. 团队实战从接盘到上线的Compose经验6.1 什么阶段什么项目适合切Compose不盲从不保守关于“什么时候可以切Compose”我给团队的建议是按阶段来别走极端。第一阶段的“探路期”最适合的是一个中低复杂度、没有太多历史包袱的新页面。比如一个设置页、一个简单的详情页数据量小、交互简单、没有太复杂的三方SDK。在这个页面里练手把Compose的基本流程跑通团队里可以建立初步的写法和规范。第二阶段的“铺开期”适合把已有项目的核心链路翻成Compose。比如一个用户从首页、列表、登录、个人中心的闭环。这个阶段会覆盖列表、状态管理、跨页面传参、网络请求状态处理等常用场景能让团队对Compose的把握更扎实。第三阶段的“深入期”才会涉及到复杂动画、自定义布局、嵌套滑动这些硬核场景。我的原则是如果一条路在View体系里已经走得很好且没有强烈的重构必要性就留着别动如果View体系的实现已经影响到开发和迭代效率“重构到Compose”才是值得的选项别为了“跟上技术潮流”去硬重构一个稳定的模块。6.2 混用View与Compose绕不开的几个架构决策混用模式下最容易犯的错误是把ComposeState到处传、把ViewModel拿到Composable里当全局变量用、或者试图在Compose里用原来的“接口回调式”的方法设计组件。我在混用阶段踩过最大的坑和处理经验主要有这么几个坑一从View发出事件到Compose。老页面里可能有一个用View实现的弹窗或输入框用户操作的结果需要通过某种方式告诉Compose的某个Composable。刚开始我们是用接口回调一层层往外传代码丑且难维护。最终换成EventBus/Flow共享事件把跨体系通信收敛到“事件总线”一条路两边都只管发和收干净又清晰。坑二Compose嵌入View时生命周期拿不准。在XML的View体系布局里嵌ComposeView时很多人不理解ComposeView也是有生命周期的它需要setViewTreeLifecycleOwner()等设置才能正常工作。实际上在常规的Activity/Fragment里ComposeView会自动绑定好但如果你在一些特殊的View子类里使用ComposeView就得手动确认生命周期是否正确绑定否则会出现“进入页面白屏、退出页面崩溃”之类的诡异问题。坑三Compose的Modifier和View的LayoutParams冲突。混合布局时经常要给ComposeView设置layout_width和layout_height如果你忘了给ComposeView设置宽高参数会让一个本来应该match_parent的Compose区域在XML里变成了wrap_content的默认尺寸UI上的表现就是内容只占了一点点剩下大片空白。6.3 一次复杂重构的复盘从RecyclerView的N个Type到LazyColumn最后用一次实际重构来收尾这个章节。之前接手过一个IM聊天页面原来的实现是RecyclerView 多Type的ViewHolder有文本、图片、语音、系统提示等七八种item类型。因为消息类型还在不断增加每加一种新类型就要改Adapter、改ViewHolder、改item点击逻辑代码维护成本越来越高所以我决定用Compose重构。重构后的核心结构大概是这样的Composable fun ConversationScreen(messages: ListMessage) { val listState rememberLazyListState() LazyColumn( state listState, modifier Modifier.fillMaxSize() ) { items( items messages, key { it.messageId } ) { message - when (message.type) { MessageType.TEXT - TextMessageItem(message) MessageType.IMAGE - ImageMessageItem(message) MessageType.AUDIO - AudioMessageItem(message) MessageType.SYSTEM - SystemMessageItem(message) } } } }重构完的直接收益有两个。第一是加新类型的成本大幅下降以前要动Adapter、ViewHolder、item布局三处现在只需要写一个新的MessageItemComposable然后在when里加一个分支就行新增一个类型的代码量从原来的上百行变成几十行。第二是整个页面的可测试性明显提升了以前想测某个item类型在不同状态下的渲染要先mock数据、跑App、手动滑到对应位置现在直接写个Preview就能看写个本地测试也方便得多。重构过程中踩的坑也有两个。一个是图片item在滑动时的闪烁——因为图片加载是异步的Combposable在首次组合时图片还没加载完会先显示占位图等图片回来后item又重组一次看起来就是闪烁。解决方案是用Coil的rememberAsyncImagePainter配合Crossfade过渡动画来做占位到图片的平滑切换。另一个坑是列表的滚动位置恢复——切走再切回来时列表位置通常会回到顶部体验不好。需要在ViewModel里维护一个ScrollPosition在onScroll事件里记录通过rememberLazyListState的scrollToItem恢复。这次重构的结论是对于一个频繁增加消息类型、需要反复调整item交互的页面Compose的收益远超成本它让新增类型的成本变得极低也让UI逻辑的维护从“改一处牵全局”变成了“改一个组件不动别的”。而如果是一个常年不变、需求稳定的普通列表页就没必要折腾重构——用了也是用了个寂寞。7. 一些个人建议和避坑心得写了这么多最后聊一点实操感受。Compose目前最大的两个问题一个是对开发者的要求更高了——以前写View体系你只要会布置控件、设监听就行现在写Compose你得理解状态管理、重组机制、稳定性和并发安全不然代码很容易“跑着跑着就崩了或者卡得不能自理”。另一个是三方SDK的适配还得靠包一层AndroidView——虽然不是没有好的方案但无形中又增加了一层来回通信的复杂度。但如果你问我“它成不成熟”我的答案是成熟到已经可以在生产环境大规模使用了但还没成熟到“无脑梭哈”的程度。它就像五六年前的Kotlin化——有人观望、有人拥抱三年后回头看那些早就切过去的项目积累的技术红利不是观望者能追得上的。我的个人建议是这几点。如果你是一个还在用View体系的存量项目从新页面开始试水渐进式混用不要一上来就全局重构如果你是一个新项目直接上Compose前提是团队里至少有一个能看懂底层原理的人不然前期排坑成本会很高如果你还在纠结“要不要学”不用纠结学就是了——这个方向不会错真正的问题只是“你的项目在什么时候把这套技术用起来最划算”。最后再分享一个小技巧。如果你准备在项目里引入Compose找一天时间让团队里每个人都自己写一遍“一个列表页点击item跳转详情页详情页改状态后返回列表刷新”的完整流程。不要用讲师给的标准demo就自己发挥跑完一遍大概就知道团队在Compose上的基础水平了也大概能预判项目接下来最大的坑会出在哪里。这个“摸底测试”的性价比比任何PPT培训都高。
返回列表