「码上开学——hencoder」Compose笔记(声明式 UI?Android 官方怒推的 Jetpack Compose 到底是什么)

1,456 阅读7分钟

开始

声明式UI:最简单的定义;实时的、代交互的预览功能;还有更强的性能和功能。这就是Android官方推出的UI框架——Jetpack Compose。

2019 年中,Google 在 I/O 大会上公布了 Android 最新的 UI 框架:Jetpack Compose。Compose 可以说是 Android 官方有史以来动作最大的一个库了。它在 2019 年中就公布了,但要到今年也就是 2021 年才会正式发布。这两年的时间 Android 团队在干嘛?在开发这个库,在开发 Compose。一个 UI 框架而已,为什么要花两年来打造呢?因为 Compose 并不是像 RecyclerViewConstraintLayout 这种做了一个或者几个高级的 UI 控件,而是直接抛弃了我们写了 N 年的 View 和 ViewGroup 那一套东西,从上到下撸了一整套全新的 UI 框架。直白点说就是,它的渲染机制、布局机制、触摸算法以及 UI 的具体写法,全都是新的。

Compose的写法

Compose从一出现,最受到官方推崇以及关注者赞扬就是它实现了声明式UI,说它比我们传统写法的「命令式UI」怎么怎么好——我们传统的ViewViewGroup那一套系统的写法叫「命令式」。但是对于大多数Android开发者来说,我们的第一个问题就是:什么是「声明式UI」?

在讲「声明式」之前,我们先看一下Compose的代码长什么样。Compose是用Kotlin来写的,它的每个控件都是一个函数调用。不如你要显示一块文字,你就这么写:

Text("Hello")

看起来好像只是调用构造函数创建了一个新对象,但这么写就已经显示出一块文字来了。

另外——这企事业并没有创建对象,这个Text()也不是一个构造函数,而是一个普通函数。

Text("Hello")
...
@Composable
fun Text(...) {
    ...
}

普通函数用大写开头干嘛?很简单,为了辨识度。Compose规定了这种大写开头的命名方式,这样我们就能一眼认出来:哦,这是个Compose的函数——或者用更官方的叫法:这是一个Composable。

到这儿有人可能就会想:这个Text()它实质上是个什么?是个TextView吗》不是的。刚才我说的过一次,Compose的渲染机制、布局机制、触摸机制全都是新鞋的,所以这个Text()的底层不是TextView,也不是任何一个原生控件,而是直接调用了更下层的绘制API,也就是Canvas那一套东西。同理,Compose里的各个组件,都是独立的新实现。

好继续说。一个函数调用是一个组件;两个函数调用就是两个组件;

Text("Hello")
Image()

多个函数组合起来,就是一个完整的界面:

Column {
    Text("Hello")
    Image()
}

这,就是Compose的写法。看完它的写法,我们就可以回到刚才的问题:什么是「声明式UI」?这段代码怎么就「声明式」了?它和我们一直以来的写法有什么区别?

首先,我们一般怎么写UI的?xml文件,对吧?比如这个界面,上下排列的一块文字和一个图片,它的等价传统写法是这样的:

<!-- 代码经过一定简化 -->
<LinearLayout>
    <TextView android:text="Hello" />
    <ImageView />
<LinearLayout>

一个LinearLayout,里面包着一个TextView和一个ImageView

看了以后什么感觉?大同小异是吧?除了名字换换、格式换换,大体上是一样的。对吧?

那为什么左边叫命令式,右边就叫声明式呢?xml命令谁了?以及,右边这写法怎么就更优秀了?我为什么要学一个看起来并没有什么本事区别的写法来为难自己?

其实所谓「声明式UI」,指的是你只需要把界面给「声明」出来,而不需要手动更新。关键在于「不需要手动更新」。比如左边这个布局里的TextView,如果它对应的数据改变了,我要怎么把新的文字更新到它?很简单:findViewById()setText()对吧?

findViewById()
setText()

而如果用Compose呢?怎么更新?不用更新。因为Compose的界面会随着数据自动更新。

Compose对界面中用到的数据自动进行订阅——不管是字符串还是图像还是别的什么,Compose全部能够自动订阅——这样当数据改变的时候,Compose 会直接把新的数据更新到界面。

var text = "Hello"
...
Column {
    Text(text)
    Image()
}

这个「自动订阅」的功能很容易使用,你只要在初始化的时候加上一个by mutableStateOf(),剩下的全都由Compose自动搞定。

var text by mutbaleStateOf("Hello")
...
Column {
    Text(text)
    Image()
}

这个神奇的功能是利用Kotlin的Property Delegation属性委托来实现的。这也在一定程度上回答了一个问题:为什么Compose只能用Kotlin写,而不能用Java?因为它用了大量的Kotlin特性,而这些特性用Java不能简单实现。注意,虽然Kotlin和Java是兼容的,Kotlin能做到的事Java也能做到,但是有些东西它「不能简单实现」就约等于不能实现了,因为不适用啊!对吧?所以Android自称永远不放弃对Java的支持,它们就这么一说,你就这么一听,不要真的就不学Kotlin,不然会越来越难受。你看着Compose不是已经在逼着我们用Kotlin了吗?

好拐回来,这就是所谓的「声明式UI」:你只要声明界面是什么样子,不用手动去更新,因为界面会自动更新。而传统的写法里,数据发生了改变,我们得手动用Java代码或者Kotlin代码去把新数据更新到界面。你给出详细的步骤,去命令界面进行更新,这就是所谓的「命令式UI」。

那么现在我们再往回拐:传统的xml写法和Compose 的Kotlin写法,为什么一个是「命令式」,一个是「声明式」?这个问题其实本身就是错的。单单一段xml代码并不能称作是命令式UI。传统写法的「命令式」并不在于xml部分,而在于Java部分:Java代码去只会、去命令界面更新,这才是「命令式」的含义所在;而Compose通过订阅机制来自动更新,所以不需要做这种「命令」,所以是「声明式」。

所以你看,不管是声明式还是命令式,跟xml和Kotlin是无关的,它们并不是语言角度的定义,也不是写法角度的定义,而是——功能角度。一个UI框架,如果可以让开发者直声明出界面的样子,而不用写各种界面更新的代码,它就是一个声明式的UI框架。换句话说,如果Android可以让我们用xml写的界面也和数据做关联,让界面自动更新而不需要开发者手写更新代码,那么它就也是声明式UI。声明式UI是一种强大的功能,而不是一种优秀的代码风格。

哎?数据和界面做关联,界面跟着数据自动更新,这不就是数据绑定吗?Android已经有这样的官方库了啊!就叫Data Binding,是吧?我用它不就得了,为什么费这么大劲去用Compose呢?

首先,对!Data Binding和Compose本质上都是通过界面对数据进行订阅来实现了界面的自动更新,但!它们是有关键区别的。区别就在于,Data Binding通过数据更新的只能是界面元素的值,而Compose可以更新界面中的任何内容,包括界面的结构。比如你用一个Boolean类型的变量控制界面中某个元素是否显示,

var text = ...
var showImage = ...
Column {
    Text(text)
    if (showImage) {
        Image()
    }
}

当你把变量的值从true变成false的时候,

var text = ...
var showImage = ...
Column {
    Text(text)
    if (showImage) {
        Image()
    }
}
...
showImage = false

这个元素会从界面中完全消失,就像从来没有出现过一样,而不是用setVisibility(GONE)这种方式从视觉上隐藏。这两种策略看起来好像区别不大,那是因为我举的例子简单,实际上这是一种机制的改变,而这种机制的改变给界面开发带来的灵活性和性能的提升是非常大的。你想一下,是不是?

总结

所以「声明式UI」还真的不是个噱头,它让Compose比传统的UI系统强大得多。而且现在除了Android的Compose之外,iOS的SwiftUI以及跨平台的Flutter也都是声明式的。声明式UI已经是一种趋势了。

版权声明

本文首发于:声明式 UI?Android 官方怒推的 Jetpack Compose 到底是什么

微信公众号:扔物线