[持续更新]细数那些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预览崩溃。


相关文章
|
存储 缓存 编译器
探索 Jetpack Compose 内核:深入 SlotTable 系统
探索 Jetpack Compose 内核:深入 SlotTable 系统
575 1
|
缓存 API Android开发
安卓现代化开发系列——传世不朽ViewModel
安卓现代化开发系列——传世不朽ViewModel
409 0
|
Android开发
android Compose中沉浸式设计、导航栏、状态栏的处理
android Compose中沉浸式设计、导航栏、状态栏的处理
2799 0
android Compose中沉浸式设计、导航栏、状态栏的处理
|
定位技术 Android开发 iOS开发
引入百度地图,安卓出现白屏问题
引入百度地图,安卓出现白屏问题
397 57
|
API Android开发 图形学
[持续更新]细数那些Compose新手容易犯的错误(二)
[持续更新]细数那些Compose新手容易犯的错误
444 0
|
XML API Android开发
构建高效的安卓应用:使用Jetpack Compose实现动态UI
【4月更文挑战第13天】 在移动应用开发领域,随着用户对流畅体验和即时反馈的期待不断上升,开发者面临着构建高效、响应式且具有丰富交互性的用户界面的挑战。传统的Android开发方法,如基于XML的布局,虽然稳定但往往伴随着较高的资源消耗和较低的开发效率。本文将探讨如何使用Jetpack Compose——一种现代声明式UI工具包,来构建动态且高效的安卓应用界面。通过深入分析Jetpack Compose的核心原理及其与传统方法的对比,揭示如何利用其强大的功能集合提升应用性能和开发效率。我们将通过实例演示如何快速构建可重用组件、实现实时数据绑定,以及优化布局渲染过程,从而为开发者提供一种更简洁、
|
Android开发
android Jetpack Navigation组件——堆栈操作和动画效果
android Jetpack Navigation组件——堆栈操作和动画效果
987 0
android Jetpack Navigation组件——堆栈操作和动画效果
|
缓存 Unix Linux
尝试安装或升级Python包时,由于设备上没有足够的空间而导致的错误
尝试安装或升级Python包时,由于设备上没有足够的空间而导致的错误
770 4
|
网络协议 关系型数据库 开发者
开源即时通讯(IM)项目OpenIM源码部署流程
开源即时通讯(IM)项目OpenIM源码部署流程
1513 0
开源即时通讯(IM)项目OpenIM源码部署流程
|
安全 编译器 Android开发
[译] Compose之稳定性(Stability)的解释(一)
[译] Compose之稳定性(Stability)的解释
398 0
[译] Compose之稳定性(Stability)的解释(一)