Android 中的布局文件是借助 XML 实现的,描述的很直观,也很容易复用,可是 XML 毕竟只是简单的标记语言,只能用来描述页面结构,而数据和页面元素的关系以及其余复杂的业务逻辑还须要经过其余程序代码主动处理。在 Activity 中,经过显式编程的方式解析 XML 文件找到你的控件,而后经过同步或者异步的方式获取控件相关的数据,最后将数据显示到控件上。这是一个很传统、很简单、颇有效的流程,可是随着需求的不断变化,愈来愈多的弊端暴露了出来:android
res/layout
这个惟一目录下,随着产品的不断迭代,该目录下的文件急剧增长,命名、查找、维护的难度变得愈来愈大res/layout
目录下的布局文件也没有办法直观地了解哪些地方正在使用这些布局文件TextView nameTextView = (TextView) rootView.findViewById(R.id.tv_name);
,页面中的每一个控件都须要这样的操做获取,还要从新再起一个符合源码规范的新名字,对于几十甚至几百个控件的页面来讲,完成这样的操做是至关繁琐至关痛苦的那什么样的 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
注解的函数,因此你以前写函数时重构,写可靠、可维护性、整洁代码的技巧一样适用于组合式函数编程
声明式编程(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
这两个名字异步
组合(composition)和继承(Inheritance)是面向对象编程中最多见的关联关系,继承是扩展类功能最简单直接的方式,可是多继承弊端太大致使除了 C++ 的大部分语言都是只容许单继承的,若是咱们把 View 系统经过继承实现,那么就会出现相似这样的问题,若是我想要个 Input,那么我继承 View,若是我想要个 ValidatedInput 那么我继承 Input,若是我想要个 DateInput 那么我继承 ValidatedInput,若是我想要个 DateRangeInput 怎么办呢?我不能继承 DateInput 由于我有两个 Date,但我又想拥有 DateInput 的能力,因此,咱们最终仍是遇到了单继承的限制。而在 Jetpack Compose 中这个问题就很简单了,咱们无非多组合一个 DateInput 而已ide
Jetpack Compose 另外一个作得比较好的地方就是封装,一个 composable 就是 给定参数,一个 composable 能够 管理状态,这是你开放你的 API 时惟一须要考虑的。另外一方面,composable 能够管理和建立状态,而后它能够将状态以及接收到的数据做为参数传递给其余 composable,子 composable 也能够经过回调的方式通知你状态的更改函数
重组(Recomposition)最基本的就是任何组合式函数都有 随时被再次调用 的能力,这也就意味着,若是你有一个很大的层级结构,当一部分层级改变后,你不须要重建整个层级。你能够利用这个特性作一些大事,好比对于以前这样的操做:工具
fun bind(liveMsgs: LiveData<MessageData>) {
liveMsgs.observe(this) { msgs ->
updateBody(msgs)
}
}
复制代码
咱们观察这个 LiveData,每次 LiveData 更新的时候都会调用咱们传入的 lambda,而后更新 UI。可是这毕竟是异步回调的形式,不符合咱们的习惯,而在 Jetpack Compose 中咱们就能够把这个关系转换过来:布局
@Composable
fun Messages(liveMsgs: LiveData<MessageData>) {
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")
}
}
}
@Composable
fun Greeting(name: String) {
Text("Hello $name!")
}
复制代码
没有参数的组合式函数是能够直接预览的,只须要添加 @Preview
注解便可
使用 Column()
函数能够竖向堆叠元素,能够经过它的 crossAxisSize
参数指定列的大小,经过它的 modifier
参数指定修饰样式
Column(
crossAxisSize = LayoutSize.Expand,
modifier = Spacing(16.dp)
) {
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}
复制代码
Container()
能够做为通用容器包裹和限制里面的元素。HeightSpacer()
能够用做留白。Clip()
能够裁剪,参数是用来裁剪的 Shape
,Shape
是不可见的。MaterialTheme()
能够给组件应用主题,而后就能够给文本应用样式了,如 Text("A day in Shark Fin Cove", style = +themeTextStyle { h6 })
@Composable
private fun TopicItem(topicKey: String, itemTitle: String) {
val image = +imageResource(R.drawable.placeholder_1_1)
Padding(left = 16.dp, right = 16.dp) {
FlexRow(
crossAxisAlignment = CrossAxisAlignment.Center
) {
inflexible {
Container(width = 56.dp, height = 56.dp) {
Clip(RoundedCornerShape(4.dp)) {
DrawImage(image)
}
}
}
expanded(1f) {
Text(
text = itemTitle,
modifier = Spacing(16.dp),
style = +themeTextStyle { subtitle1 })
}
inflexible {
val selected = isTopicSelected(topicKey)
SelectTopicButton(
onSelected = {
selectTopic(topicKey, !selected)
},
selected = selected
)
}
}
}
}
复制代码
这是一个包含三个元素的列表项,看起来还算直观,可是仍是感受哪里有点别扭
因为借助了 Kotlin 的 trailing lambda 表达式的语法,代码最终看起来仍是可以表达出某种层次或结构的
Jetpack Compose 是个颇有趣的尝试,让我看到了 Android 新的构建 UI 方式的可能,从语法上来看仍是有一些 HTML 和 Flutter 的影子的,对于页面复杂嵌套层级过深状况下的处理应该还有很长一段路要走,Flex 布局可否解决这个问题,Flex 布局是否真的适合 Android,Jetpack Compose 的性能如何保证,调试是否方便,Gap Buffer 的算法是否比 Diff 算法有优点等等都是须要面对,须要思考,须要时间去解决的问题
总之,Jetpack Compose 目前只是一个尝试,还缺乏足够的控件支持,还缺乏足够的工具支持,还缺乏足够的稳定性,不过我很乐意看到这种新的尝试出现,刀耕火种的时代老是要过去的