[持续更新]细数那些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之排错总结(持续更新,因为还在学习)
|
6月前
|
开发者 C++ UED
你以为的Bug VS 实际的Bug:解密程序开发中的意外之旅
作为开发者,我们在日常开发过程中经常会遇到各种各样的Bug,有些Bug可能很容易发现并解决,但也有一些Bug让人感到困惑摸不到头脑,甚至是无厘头Bug,就像我们以为的Bug与实际的Bug之间的差异一样,让人头大。所以我们在日常开发过程中,一定要细心、细致、细顾,在面对任何Bug的时候都要抱着敬畏的心态去解决,因为我们永远不知道在实际程序开发中的意外是啥,有什么意外在等着我们去发现和解决。那么本文就来讨论分享一下开发者在工作过程中遇到的“你以为的Bug”与“实际的Bug”之间的差异在哪里?,然后通过一个有趣的比喻,我们将深入分析这些不同类型的Bug,还有就是在解决问题时的重要性和挑战。
75 1
你以为的Bug VS 实际的Bug:解密程序开发中的意外之旅
|
人工智能 IDE 开发工具
走近Python编程的“BUG”世界
走近Python编程的“BUG”世界
68 0
|
API Android开发 图形学
[持续更新]细数那些Compose新手容易犯的错误(二)
[持续更新]细数那些Compose新手容易犯的错误
200 0
|
机器学习/深度学习 移动开发 前端开发
想加入大厂?看这篇文章也许会帮助到你
相信加入互联网大厂是每个程序员梦寐以求的事情,无论是从工作环境、员工福利,或者说是技术氛围以及接触到的人所给你带来的一些好的机遇,都是值得我们去追求的,因此程序员可以在职业生涯初期、或者在整个职业生涯中加入过大厂,无论对自己的履历还是阅历都是很有帮助的一件事。
105 0
想加入大厂?看这篇文章也许会帮助到你
|
存储 消息中间件 安全
「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)
「避坑宝典」为大家分享一下笔者在 2022 年所遇到“匪夷所思”的 Bug 趣事(上)
104 0
|
前端开发
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)(中)
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)
ADS2020安装陷阱你学废了吗,小白狂喜教程
ADS2020安装陷阱你学废了吗,小白狂喜教程
847 0
ADS2020安装陷阱你学废了吗,小白狂喜教程
|
编解码 算法 PyTorch
解决方案:炼丹师养成计划 Pytorch+DeepLearning遇见的各种报错与踩坑避坑记录(二)
解决方案:炼丹师养成计划 Pytorch+DeepLearning遇见的各种报错与踩坑避坑记录(二)
解决方案:炼丹师养成计划 Pytorch+DeepLearning遇见的各种报错与踩坑避坑记录(二)