[持续更新]细数那些Compose新手容易犯的错误(一)

简介: [持续更新]细数那些Compose新手容易犯的错误

笔者作为一个日常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的状态和方法的能力,看起来非常完美,代码跑起来也没问题。

但是,这样的方式是错误的,同时也会带来内存泄漏的隐患。

image.png

基于官方文档,笔者总结出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,会出现无法预览的问题,如下:

image.png

image.png

出现这个问题的原因是预览系统无法正确实例化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预览崩溃。


相关文章
|
前端开发
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)(下)
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)
|
JavaScript 搜索推荐 Java
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)(上)
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)
|
5月前
|
前端开发 Java 数据库连接
如何顺利完成毕业项目看完这篇文章有你想要的!
如何顺利完成毕业项目看完这篇文章有你想要的!
|
8月前
|
存储 JSON 算法
新手避坑:盘点使用ClickHouse最容易犯的12个错误
在这篇文章中,我们突出了新手用户遇到的最常见的12个问题,这些问题是由于在使用ClickHouse的过程中,不遵循最佳实践,甚至反最佳实践而导致的。对于每一个问题,我们都推荐了一个解决方案或正确的使用方法。
2637 18
新手避坑:盘点使用ClickHouse最容易犯的12个错误
|
9月前
|
API Android开发 图形学
[持续更新]细数那些Compose新手容易犯的错误(二)
[持续更新]细数那些Compose新手容易犯的错误
116 0
|
12月前
|
机器学习/深度学习 移动开发 前端开发
想加入大厂?看这篇文章也许会帮助到你
相信加入互联网大厂是每个程序员梦寐以求的事情,无论是从工作环境、员工福利,或者说是技术氛围以及接触到的人所给你带来的一些好的机遇,都是值得我们去追求的,因此程序员可以在职业生涯初期、或者在整个职业生涯中加入过大厂,无论对自己的履历还是阅历都是很有帮助的一件事。
77 0
想加入大厂?看这篇文章也许会帮助到你
|
12月前
|
程序员 测试技术 开发工具
程序员成长第十篇:从阅读代码开始
程序员成长第十篇:从阅读代码开始
172 0
|
存储 消息中间件 安全
「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)
「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)
81 0
|
前端开发
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)(中)
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)
|
机器学习/深度学习 算法 编译器
明天省赛,我都还不太熟悉Dev - C++,怎么切换成C++11了?它的报错看不懂呀,那花八分钟看看这篇文章吧~解决你的困惑。
明天省赛,我都还不太熟悉Dev - C++,怎么切换成C++11了?它的报错看不懂呀,那花八分钟看看这篇文章吧~解决你的困惑。
706 0
明天省赛,我都还不太熟悉Dev - C++,怎么切换成C++11了?它的报错看不懂呀,那花八分钟看看这篇文章吧~解决你的困惑。