大家好,我是扔物线朱凯。
最近郑州疫情,我们全员居家办公。在家闷头做课程的时候,我被 Slack 上的同事要求写个介绍课程的宣传文。我觉得如果只介绍课程的话可能会没人看,那就白写了。思索过后,我决定分享一下自己学 Compose 的经验,跟大家讲一下我认为最完美的学习 Compose 的路线。如果能让读者觉得有用,酣畅淋漓地读着,一不留神被下面的广告部分吸引,岂不是两全其美?
或者更确切地问:你要现在就开始学 Compose 吗?
Compose 未来一定会取代 View 系统的写法,成为 Android 开发的主流方案。但就当下来说,并不是任何人都需要现在赶紧学的。我的观点是:想做 Compose 先行者、或者公司已经在用 Compose,不学不行的,学。更具体的,大家可以看我的这个视频:
Compose 的知识体系非常庞大,我已经近乎全职地研究了快两年了,到现在课程终于尾声。下面是我对于 Compose 学习的重点总结,希望可以帮到大家。
Compose 在写法上和 Android 传统的 View 方案很不一样,也就是所谓的「声明式 UI」。
声明式的写法确实很方便,但因为和我们传统的写法(所谓的「命令式」)差别非常大,所以学 Compose 的第一步,最好是先自己去写几个简单的 demo,看看 Compose 的界面代码大概是长什么样的——比如文字怎么写、按钮怎么写、线性布局怎么写、滑动布局怎么写、点击监听怎么设置。
关于「声明式 UI」的介绍,我有一个视频
最好别只看视频,自己去写个代码感受一下。不用太多太复杂,因为复杂的界面需要更深的知识,等你学完之后自然就知道怎么写了,太早纠结于「在 Compose 里这种效果要怎么写呀?让我去搜搜问问」反而会耽误你的时间。所以随便了解一下,写个大概就行了。
Compose 的声明式写法,不可避免地引入了它的状态机制。声明式 UI 最强的地方在于,通过各层级的状态变量的控制,让工程师不需要写任何的界面更新的代码(例如 nameView.text = newName
这样的),而是仅仅对各种状态的值进行更新,界面就能实现自动的变化,包括动画。
而状态机制,就是这背后的技术基石。所以在了解了 Compose 的基本用法之后,尽早把它的状态机制搞清楚,对于学习其他更全面、更深入的 Compose 知识是非常重要的。
关于 Compose 的状态机制,有几个重点:
MutableState
类和 mutableStateOf()
函数用法和工作原理。了解到工作原理就好,这能让你对于「自动更新」有更清楚的认识,从而对于「什么时候会自动更新、怎样可以触发自动更新」具备预判的能力。List
、Map
这些集合类,它们也有对应的 mutableStateListOf()
、mutableStateMapOf()
这样的函数。它们不能和普通的数据类型一样使用 mutableStateOf()
的原因和 Compose 的自动更新机制有关,如果有意往「高级」的方向突,可以了解一下,否则的话不看那么深也行。remember()
函数和 Compose 的重组作用域的了解。这是一个关于性能的知识点。CompositionLocal
。这是 Compose 里的「针对 Composable 函数调用的、具有穿透能力的局部变量」,一般用来为嵌套调用的组件提供上下文信息。也是必备知识之一。@Stable
在特殊情况下进行性能优化;derivedStateOf()
:一个提供「状态链条」功能的函数,对于 A 状态的改变触发 B 状态的更新的场景适用,正确使用可以提高复杂场景下的性能。这个函数写起来很简单,关键是它的使用场景背后的机制要搞明白。可能会比较难想清楚,别着急,多想想。上面这些知识全部掌握,对于 Compose 的状态机制就可以达到高手的级别了。但如果你不打算做公司的 Compose 一把手,「高级知识」那两条暂时缓缓也没问题。
动画的知识对于 UI 来说非常重要,但它的理解需要以状态机制的知识作为基础,所以搞定了状态机制之后,你可以去学动画。
Android 传统的 UI 里,属性动画是个历史悠久的话题,也是非常重要的一块知识。到了 Compose 里,动画依然重要。原因很简单,因为这是软件开发的硬需求。
Compose 的动画写起来很简单。它的核心知识包括这几块:
animateXxxAsState()
函数。这个初看会觉得方便到离谱的程度,把属性用 animateXxxAsState()
的方式赋值,它们改变的时候就会自动用动画的方式来呈现了。背后的原理是悄悄地开启了协程。Animatable
类。它是 animateXxxAsState()
的底层实现类,而由于 animateXxxAsState()
的「自动」,所以我们只能用它来写状态转移时自动渐变状态的动画,而不能精确地定制动画流程。而这个更底层的 Animatable
,就可以帮我们做精确定制的动画。AnimationSpec
:对于动画的速度曲线的限制,传统属性动画用的是 Interpolator
,而在 Compose 用的是这个 AnimationSpec
。animateDecay()
。AnimationSpec
也不适用于它,它要用专门的 DecayAnimationSpec
。Transition
的东西,它是用于设置多属性的状态切换型动画的,比如一个头像点击之后一边放大一边移动一边旋转,用 animateXxxAsState()
就没有 Transition
用起来方便。另外,Compose 还提供了一些更方便的 Transiton
延伸的 Composable 函数:AnimatedVisibility()
:自动的出现和消失。Crossfade()
:省事版的 AnimatedVisibility()
,效果是预设的淡入淡出。AnimatedContent()
:更复杂的批量控制组件的出现和消失。动画部分没有很难的单点知识,主要是要掌握各个技术点的定位,以及几种动画实现方式的知识结构:Animatable
是最底层,它上面是 Transition
和 animateXxxAsState()
。
Modifier
Modifier
是 Compose 里最大的一块知识,没有之一,Compose 里大多数的功能都是靠 Modifier
来实现的。
Modifier
本质上是 Compose 里的一种提供 UI 组件的通用属性的方式。所谓通用属性,即任何一个组件都可能、都可以拥有的属性,例如尺寸;与之相对的是个性化属性,例如文字组件里的字符串,个性化属性是用 Composable 函数的参数来提供的。
而 Compose 里的 Modifier
提供的属性,不仅限于「组件所要显示的内容」,而是包括各种与组件的显示和交互有关的属性。例如点击监听,用的是 Modifier.clickable()
函数;设置类似传统写法里的 LinearLayout
里的 weight
属性的效果,用的是 Modifier.weight()
函数。
Modifier
的功能很强大,但同样也是因为 Modifier
的强大,所以它的相关知识也很多。我翻遍了 Compose 的相关源码,总结出 Modifier
的知识大致分类如下:
Modfiier.then()
:用于合并(或者说连接)两个 Modifier
。更下层的两个相关类型是 CombinedModifier
和 Modifier.Element
,了解这两个东西会对 Modifier
的工作逻辑以及在各种场景下的性能表现有更清楚的了解。Modifier.composed()
:用于把一个 Modifier
包进一个工厂函数,以便复用。比较实用,但就算不用也可以实现某些需求,直到有一天你发现「卧槽,竟然还有个这么方便的东西!」所以一定要花时间了解一下。LayoutModifier
:看起来像是「Compose 里的自定义布局」的工具,其实只是用于对单个 Composable 组件进行尺寸修饰的。类似传统写法里对自定义 View 重写 onMeasure()
来进行尺寸自定义,但比这种写法的功能更弱一些。DrawModifier
:用于绘制的 Modifier
。所有组件的绘制都是用的它,自定义绘制也是用的它。PointerInputModifier
:用于设置触摸反馈逻辑的 Modifier
。所有组件的触摸都是用它写的逻辑,自定义触摸反馈也是用的它。需要注意的是,Compose 里使用的协程来设置触摸反馈逻辑的,所以和自定义 View 的触摸反馈是完全不一样的写法,但其实思维逻辑简单得多,只是需要你换个思考方式而已。所以上手的时候不要害怕,试一试你会发现复杂的自定义触摸逻辑你用 Compose 来写并不是很难(起码比 onTouchEvent()
简单)。ParentDataModifier
:用于处理具有父子关系的组件的界面的 Modifier
,给父组件提供子组件的信息,来辅助测量。例如前面提到的 Modifier.weight()
,内部实现所有的就是一种 ParentDataModifier
。SemanticsModifier
:用于提供语义树设置的Modifier
。所谓语义树,其实就是组件树,只不过在某些情况下我们可以对某个子树进行合并,例如「把一个内部包含图片和文字的按钮,合并成一个组件」,来简化语义逻辑。主要用于两种场景:开发测试和无障碍功能的功能优化。这个我感觉比较难用几句话说清楚,但我的课程里的这节课也没开放试听,大家可以试着上网搜一下这个词。OnRemeasuredModifier
:重新测量的监听器,在重新测量发生之后会触发它的回调函数。OnPlacedModfier
:重新布局的监听器,在重新布局(即位置摆放)发生之后,它的回调函数会被触发。ModifierLocal
:以及跟它相关的 ModifierLocalProvider
和 ModfieirLocalConsumer
:用于提供和 CompositionLocal
类似的「特殊版本的局部变量」的功能,相当于 Modifier
中的、具有穿透功能的属性。同样适用于提供上下文,不过是 Modifier
之间的上下文。例如对于 WindowInsets
相关的 Modifier
,可以在多层 Modifier
之间共享消耗掉的、剩余可用的 WindowInsets
;再如对于嵌套滑动的布局里,对于已消耗的手指滑动唯一的内外层共享。这个东西有点难,但是非常有用,最好搞明白,可以让你把很多事的做法变得简单得多。Modifier
的难点主要在于两点:搞明白 Modifier
和函数参数在适用场景上的区别(上面说了,Modifier
适用于通用属性,函数参数适用于个性化属性);Modifier
有很多种,每一种都是一个单独的知识,并且还有几类是有一些理解难度的,所以总的知识量比较大。但是当你把这一块的知识学完,你的 Compose 水平就已经在比较高的位置了。
Compose 的界面刷新靠的是一种叫做「重组」的机制,它本质上就是把界面元素的生成逻辑重新执行一遍,因为 Compose 里的所有界面组件其实都是函数,而界面的生成和刷新靠的也是这些界面函数的调用。但这种「重新执行」会导致性能问题,即没有发生改变的界面组件也被重新执行,导致了执行效率的下降,最终结果就是界面刷新的缓慢、用户操作和动画的卡顿。所以 Compose 的「i重组」机制里,加入了很多的优化,例如局部的重新调用,而非整个界面的重新调用。
而这种「局部重新调用」的发生时机是不可预知的,并且发生的次数也是无可预知的,这就导致如果我们在 Composable 组件的代码里加入对外界有影响的逻辑(例如把某个外部变量的值加一)——这个在编程领域被称作 side effect,或者中文叫副作用——有可能发生我们意想不到的结果。
但有时候我们又会有一些「界面的显示触发某些外部改变」的情况,这就是 Compose 的 side effect 相关的函数的用处所在。这类函数一共有三个:
SideEffect()
DisposableEffect()
LaunchedEffect()
LaunchedEffect()
就是在 Composable 函数内部开启协程的方式,所以学习 Compose 的时候,这个函数一定不能漏——实际上,这三个函数都不能漏。也不多,一个个学呗?rememberCoroutineScope()
函数来提供一个可供外部使用的 CoroutineScope
,这样就可以在任何时候(例如点击监听器里)手动触发协程。所以这个 rememberCoroutineScope()
也需要看看。rememberUpdatedState()
的函数,它是对于 DisposableEffect()
和 LaunchedEffect()
的一个非常有用的辅助。它和前面提到的 derivedStateOf()
的特点有点像:写起来很容易,重点是知道它怎么用。所以,不要小看它,花点时间研究一下它的用法。例如:
以上就是我在近乎全职地研究了两年 Compose 之后,对于「应该如何学习 Compose」做出的总结和建议。你按照这份总结和建议把里面的知识刷了(看官方文档、看源码、看网上的博客、看我的公开视频,加上自己的思考和练习),应该可以得到不错的结果。如果这篇文章帮到了你,还请帮忙点赞转发一个,让更多人看到。