笔者作为一个日常Jetpack Compose开发者,对Compose的理解也在逐渐加深中,最近回顾当初学习和实践的过程中,犯了不少错误和踩了很多坑,本篇文章作为小总结分享给大家,同时文章会持续更新,也欢迎评论区或者私信给笔者投稿,谈谈你使用Compose过程中踩过的那些坑。
一、ViewModel传递到子可组合项
Jetpack Compose的状态管理是极其重要的一环,当一个可组合项的状态较少时,我们需要使用状态对象来封装状态,而屏幕级的状态对象我们最常用的就是ViewModel
。
关于状态管理可以参考下面这篇开发者文档:
状态容器和界面状态 | Android 开发者 | Android Developers (google.cn)
继续回到话题,使用ViewModel
来管理屏幕级状态时的代码大致如下所示:
class MyScreenViewModel(/* ... */) { val uiState: StateFlow<MyScreenUiState> = /* ... */ fun doSomething() { /* ... */ } fun doAnotherThing() { /* ... */ } // ... } @Composable fun MyScreen( modifier: Modifier = Modifier, viewModel: MyScreenViewModel = viewModel(), state: MyScreenState = rememberMyScreenState( someState = viewModel.uiState.map { it.toSomeState() }, doSomething = viewModel::doSomething ), // ... ) { /* ... */ }
可以看到ViewModel
通过参数的方式直接传递到了MyScreen
可组合项中,这样做是没问题的而且非常便利,可组合项可以通过ViewModel
直接获取到所需的状态,同时也可以通过ViewModel
的方法来访问各种逻辑函数。
正因为这样太便利了,很多Compose新手会直接把ViewModel
进一步传递到子可组合项,让子可组合项也能“便利”地访问到状态和逻辑函数,写出这样的代码:
@Composable fun MyScreen( modifier: Modifier = Modifier, viewModel: MyScreenViewModel = viewModel(), state: MyScreenState = rememberMyScreenState( someState = viewModel.uiState.map { it.toSomeState() }, doSomething = viewModel::doSomething ), // ... ) { /* ... */ SonComposable(viewModel) } @Composable fun SonComposable( viewModel: MyScreenViewModel = viewModel(), ){ /* ... */ }
🤩哇喔,通过参数将ViewModel
传入了子组合项,让子组合项也拥有访问ViewModel
的状态和方法的能力,看起来非常完美,代码跑起来也没问题。
但是,这样的方式是错误的,同时也会带来内存泄漏的隐患。
基于官方文档,笔者总结出ViewModel
在Compose中的正确方式:
1.ViewModel仅用于最顶层的屏幕级可组合项,即离Activity或者Fragment的setContent{}方法最近的那个可组合项。
2.遵循单一数据源规范,ViewModel将状态传递给子可组合项,子可组合项将事件向上传递给顶层的可组合项,不能将ViewModel直接传递给子可组合项。
注:很久以前官方文档还会提到ViewModel可能会导致子可组合项的内存泄漏,因为ViewModel的生命周期会比子可组合项更长,一些lambda或者匿名方法会导致可组合项被ViewModel持有导致内存泄漏。
我们按照原则(状态下传,事件上传)将代码改造成如下即可:
@Composable fun MyScreen( modifier: Modifier = Modifier, viewModel: MyScreenViewModel = viewModel(), state: MyScreenState = rememberMyScreenState( someState = viewModel.uiState.map { it.toSomeState() }, doSomething = viewModel::doSomething ), // ... ) { /* ... */ SonComposable(viewModel.content, onContentChange = { viewModel.onContentChange(it) }) } @Composable fun SonComposable( content:String, onContentChange:(String)->Unit={} ){ /* ... */ }
二、不恰当的参数导致@Preview不能预览
也许你的一些可组合项会出现无法预览的问题,导致这个问题的原因有很多,大多数都是一个原因导致的:即预览系统遇到了异常。
- 一个常见的错误就是对使用
ViewModel
的屏幕级可组合项使用@Preview
,会出现无法预览的问题,如下:
出现这个问题的原因是预览系统无法正确实例化ViewModel,因为ViewModel的实例化依赖于运行中的android系统,而预览系统实际上是一个阉割版的android系统,它只有和UI相关的代码。
解决方案:
对屏幕级的可组合项抽离出一个只依赖于状态类的的子可组合项,将@Preview下沉到该子可组合项,屏幕级子可组合项不预览。
@Composable fun MvRankScreen( viewModel: MvRankViewModel = viewModel(), ){ MvRankContent(viewModel.rankState) } @Composable private fun MvRankContent( rankState:RankState ){ /* ... */ } @Composable @Preview private fun PreviewMvRankContent(){ MvRankContent(remember{RankState()}) }
如上所示,将MvRankScreen
的内容抽离出一个MvRankContent
出来,然后MvRankContent
只使用ViewModel
传递下来的状态类,这样只预览MvRankContent
,就可以解决ViewModel
导致无法预览的问题。
- 另外一个常见的错误就是使用了项目中的其他类,该类只能android运行时才能获取,也会导致预览系统的崩溃,例如下面的一个类:
object MyClass{ fun getDesc():String{ return MyApplication.getInstance().getDesc() } }
该类的方法会从自定义的Application的实例获取一个字符串参数,而这个自定义的Application在预览系统中是不存在的,在Compose中直接使用此类也会导致预览系统的错误。
解决方法:
和一些View依赖于运行时才能获取的状态导致无法预览的问题类似,Compose也提供了一些方法来区分项目实际运行中和预览中的状态,如下所示:
@Composable fun MyTest(){ Text( text=if(LocalInspectionMode.current) "预览中" else MyClass.getDesc() ) }
我们可以通过LocalInspectionMode.current
来判断当前Compose是否运行于预览系统中,如果处于预览系统,我们使用固定的字符串,防止了直接访问getDesc()导致Compose预览崩溃。