在布局文件中我们给每个控件起一个名字(ID),而在程序中我们需要根据这个 ID 查找我们需要的具体控件,如TextView nameTextView = (TextView) rootView.findViewById(R.id.tv_name);页面中的每个控件都需要这样的操作获取,还要重新再起一个符合源码规范的新名字,对于几十甚至几百个控件的页面来说,完成这样的操作是相当繁琐相当痛苦的。

耦合严重

随着应用迭代,XML布局文件和源码都要同步地进行更改,也就是说每加一个控件,XML文件和源码都要相应地进行更改,如果XML中移除了某个控件而源码没有更改就会出现运行时异常,所以它们实际上是强耦合的。

方式不统一

一个页面布局既可以通过XML文件的方式静态指定,也可以通过编写源码的方式动态创建,这两种截然不同的方式虽然都可以实现页面布局,但毕竟是不同的语言,不同的系统,很难统一管理和维护。

架构缺陷

数据驱动视图思想的实现需要视图可以方便的与数据进行单向或者双向的绑定,只有Data Binding技术可以实现XML里插入代码,完成和数据的绑定,但是这样的操作就像JSP在HTML文件中插入Java代码一样,虽然简单直接,但是代码逻辑的连贯性、一致性以及可维护性会面临前所未有的挑战。

那什么样的UI构建方式才能避免上述的问题呢?什么样的构建方式才是简单有效的呢?我相信大多数人的回答都是 “声明式(declarative)”,在源码中声明式的构建UI既直观又不会损失源码的能力。但是这个愿景在实现上却又困难重重,怎么让Java或Kotlin拥有声明式语法的能力,怎么让排版布局更加的简洁直观,怎么避免 UI 逻辑和业务逻辑的耦合等等都是需要重点解决的问题,而我觉得Jetpack Compose是个很不错的尝试。

架构思想


关注点分离

关注点分离(separation of concerns)是最常见最出名的软件设计原则,也是每个开发者都应该了解并遵循的,其实关注点分离最初是对另外两个词的概括:耦合(coupling)和内聚(cohesion)。理论上,当我们写代码时,我们会把应用看成多个模块,而且还可能把每个模块看成多个单元,这些模块或单元之间的依赖关系就是耦合,也就是说,如果我在某处对一些代码进行了更改,那么我还必须对其他文件进行多少更改?

所以我们一般的想法就是尽可能的减少耦合。有时耦合是隐式的,那些我们依赖的依赖或者其他我们依赖的东西实际上是不确定的,但是还是会因为我们的更改而被破坏。另一方面,内聚指的是模块中的单元如何相互归属,它们彼此相关,高内聚通常被视为一件好事。因此关注点分离就是将尽可能多的相关代码组织在一起,以便我们的代码可以随着时间推移而更好地维护,随着应用的成长而真正地扩展 。

在Android中一般的做法是用XML布局显示东西,用ViewModel给这个布局提供数据,事实上这里隐含了很多依赖,ViewModel和布局之间存在很多耦合,如果XML中新增了控件,ViewModel中也要新增对应的数据,这个关系是隐式的,但又是真实存在的。如果我们用相同的语言如Kotlin构建UI,那么这个关系就可能会变成显式的了,甚至我们接下来开始重构一些代码,将一些东西移到它们所属的地方,实际上减少了某些耦合,增加了一些内聚。

你可能会问了,这不是把业务逻辑和UI混在一起了吗?好吧,我们换个角度看一下,一些业务逻辑难道不是UI的一部分吗?

其实任何框架都不能完美地帮你分离你的关注点,也不能阻止你将逻辑和UI混在一起,但是Jetpack Compose提供了工具可以让你很容易进行分离,这个工具就是组合式函数(composable functions),一个加了@Composable注解的函数,所以你之前写函数时重构,写可靠、可维护性、整洁代码的技巧同样适用于组合式函数。

声明式 vs 命令式

声明式编程(declarative)和命令式编程(imperative)是不同的编程思想,比如有个需求是这样的,未读消息数是0的时候显示一个空信封的图标,有几个消息的时候在信封图标上加个信件图标和消息数badge,消息数超过100时再加个火苗并且badge不再是具体数字而是99+。如果是命令式编程,我们肯定要写一个根据数量进行更新的函数:

fun updateCount(count: Int) {

if (count > 0 && !hasBadge()) {

addBadge()

} else if (count == 0 && hasBadge()) {

removeBadge()

}

if (count > 99 && !hasFire()) {

addFire()

setBadgeText(“99+”)

} else if (count <= 99 && hasFire()) {

removeFire()

}

if (count > 0 && !hasPaper()) {

addPaper()

} else if (count == 0 && hasPaper()) {

removePaper()

}

if (count <= 99) {

setBadgeText("$count")

}

}

弄清楚如何调整 UI 以使其呈现正确的状态,实际上可能还有很多极端情况,这个逻辑并不简单,但是这已经算是相对简单的例子了。而如果你用声明式的方式写这段逻辑那么会是这样的:

@Composable

fun BadgeEnvelope(count: Int) {

Envelope(fire = count > 99, paper = count > 0) {

if (count > 0) {

Badge(text = if (count > 99) “99+” else “$count”)

}

}

}

你会发现至少在UI操作上来说声明式编程要更加直观,更加简洁。

而UI开发者最关心的是什么呢?对于给定的数据UI该怎么显示?怎么响应事件让UI进行交互?UI随着时间应该怎样变化?

有了声明式编程,有了Jetpack Compose,我们不再需要考虑UI随时间的变化,这是最重要最关键的点,因为在我们拿到数据后我们就定义了它在各个状态下应该怎么展示,之后框架会控制如何从一个进入另一个状态,即 “根据提供的参数来描述UI”。组合式函数,是个函数定义,但是它在一个地方描述了UI所有可能的状态,而且是本地定义的,这就是组合(composition),因此有了Compose和@Composable这两个名字。

组合 vs 继承

组合(composition)和继承(Inheritance)是面向对象编程中最常见的关联关系,继承是扩展类功能最简单直接的方式,但是多继承弊端太大导致除了C++的大部分语言都是只允许单继承的,如果我们把View系统通过继承实现,那么就会出现类似这样的问题,如果我想要个Input,那么我继承View,如果我想要个ValidatedInput那么我继承Input,如果我想要个DateInput那么我继承ValidatedInput,如果我想要个DateRangeInput怎么办呢?

我不能继承DateInput因为我有两个Date,但我又想拥有DateInput的能力,所以,我们最终还是遇到了单继承的限制。而在Jetpack Compose中这个问题就很简单了,我们无非多组合一个DateInput而已。

封装

Jetpack Compose另一个做得比较好的地方就是封装,一个composable就是 给定参数,一个composable可以 管理状态,这是你开放你的 API 时唯一需要考虑的。另一方面,composable可以管理和创建状态,然后它可以将状态以及接收到的数据作为参数传递给其他composable,子composable也可以通过回调的方式通知你状态的更改。

重组

重组(Recomposition)最基本的就是任何组合式函数都有 随时被再次调用 的能力,这也就意味着,如果你有一个很大的层级结构,当一部分层级改变后,你不需要重建整个层级。你可以利用这个特性做一些大事,比如对于之前这样的操作:

fun bind(liveMsgs: LiveData) {

liveMsgs.observe(this) { msgs ->

updateBody(msgs)

}

}

我们观察这个LiveData,每次LiveData更新的时候都会调用我们传入的lambda,然后更新UI。但是这毕竟是异步回调的形式,不符合我们的习惯,而在Jetpack Compose中我们就可以把这个关系转换过来:

@Composable

fun Messages(liveMsgs: LiveData) {

val msgs = +observe(liveMsgs)

for (msg in msgs) {

Message(msg)

}

}

在这里我们调用了observe()函数,它做了两件事,首先是解封装LiveData来返回它的当前值,这也就意味着你可以在函数体中直接使用这个值。其次,它还隐式地将LiveData订阅到这个它会被解封装的组合式函数作用域中。这也就意味着,我们不再需要传递lambda表达式了,我们只需要知道这个组合式函数每次在LiveData变化时都会重组就行了。让我们再次比较上面两段代码,虽然在代码量上没有什么差异,但是在思想上后者要更加符合我们的思维习惯,更加直观。

数据驱动视图

数据驱动视图的思想既能简化UI操作又能保证数据展示的一致性,而Data Binding对于数据驱动视图的尝试虽然有效,但是并不是十分优雅,一个Model可以插入到XML中,可以进行一些简单的处理,而如果让视图跟随Model变化还需要将Model转化成Observable,这个转化是需要手动完成的。而Jetpack Compose对于数据驱动视图的尝试要更优雅一些,如这里的一个计数器功能:

@Composable

fun Counter() {

val count
= +state { 0 }

Button(

text = “Count: ${count.value}”,

onClick = { count.value += 1 }

)

}

state函数可以直接返回包裹了给定值的State状态类实例,State类用了@Model注解,而@Model注解就意味着这个类的所有属性的读写操作都是observable的,Jetpack Compose做得就是当你执行你的组合式函数时,如果你读取了一些Model实例,那么Jetpack Compose将自动订阅所在的作用域以便进行Model的读写。因此这个例子中的Counter是独立自给的,每次Model的值发生更改时Counter都会重组。

使用

组合式函数

Jetpack Compose是建立在组合式函数(composable functions)的基础上的,这些函数可以让你以编程的方式定义UI(通过描述它的形状和数据依赖),而不是关注UI的构建过程 一个组合式函数只能被另一个组合式函数调用,所以组合式函数需要添加@Composable注解。

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContent {

Greeting(“Android”)

的基础上的,这些函数可以让你以编程的方式定义UI(通过描述它的形状和数据依赖),而不是关注UI的构建过程 一个组合式函数只能被另一个组合式函数调用,所以组合式函数需要添加@Composable注解。

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContent {

Greeting(“Android”)

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐