Compose 呼之欲出,Flutter 展露锋芒

3,711 阅读13分钟

Compose 呼之欲出,Flutter 展露锋芒

扶我起来,我还能学

新技术层出不穷,作为开发者,一入此门中,从此不是在学习中,就是去学习的路上。 而最近一年呼声最高的莫如 Flutter 和 Jetpack-Compose 了,今天就聊一聊它们,没有特定的思路,想到哪就聊哪吧。另外,整篇文章个人观点性比较强,又限于自己技术格局,可能会有很多不当甚至错误的地方,不喜勿喷,有错请纠。

要不要学新技术,有时候真的不是选择题!

每当有新鲜技术出炉,会有很多人感到迷茫,所以你经常会在技术社区看到:Kotlin 要不要学?Flutter 怎么样值不值学?有没有必要上 MVVM?Jetpack 坑多不多?

对于新技术 不学 看上去似乎是个选择题,总有一天你回头时就会发现,有时候它们真的不是选择题。只要你还在开发的这条道路上,它总在某个路口等着你。学,是个必然的选择,而你能选择的只是学的时间节点而已。

拿 Kotlin 来说,前两年可能还有很多摇摆的声音。而现在来看,官方推出很多最新框架,基本上已经是全用 Kotlin 来写的了,对于 LiveData、ViewModel 来说,可能你还能反抗一下继续使用 Java 来写,其实你也能发现后面的很多新特性新写法更多的是和 Kotlin 的特性(比如协程)结合在一起的。而对于稳定版呼之欲出的 Compose UI 套件来说,Kotlin 已经成了你的唯一选项。所以,当初 Google 将 Kotlin 推到 Android 开发的第一阵营时,从不是多给了你一个新选项,给你的只是学习它的时间。

而本文主要是为了聊 Compose 和 Flutter,后面的部分主要在它们之间从各个方面来做分析对比。

Compose 和 Flutter 是不是选择题?

对于 Flutter 来说,虽不好说它究竟能走多远,但就目前形势来看,它已经成功了。对于各种跨平台技术来看,Flutter 做为一个后来者,现在俨然有成为跨平台中最好的选项的趋势。而在国内的发展趋势也是如火如荼,各家大厂都在研究并应用在自家应用当中,对于初创公司很多甚至用它作为主开发,毕竟跨平台的优点不言自明。

而对于 Compose 来说,连稳定版尚未出炉,却是众人翘首以待。

那它们是不是选择题?(这里的选择不是指,它们之间哪个好,要学哪一个,而是都单独的来说,是不是要学)现在这个答案虽没人敢给你下一个定论,但能明确告诉你的,它们已经后面的趋势所向,而也必然是后面热门的点,至于能不能成为主流,且不好说。

个人观点:其实能不能成为主流不是重点,作为一个热门的趋势就已经值得你去学了;对于 Compose 可以姑且等等(也只是等等),毕竟还不能用在生成环境,且有待实际验证。而 Flutter 来说,哪怕此刻开始学,已经略迟于人,还在犹犹豫豫的可以投入学习了,哪怕单纯从技术理论上来说,就值得你尝试一下。

编程思想的变化

从 Win32 到 Web 再到 Android 和 iOS,框架通常使用一种命令式的编程风格来完成 UI 编程,这可能是多数人最熟悉的风格。随着 UI 框架的发展,声明式 UI 编程已经成为潮流。而 Flutter 和 Compose 都作为新推出的 UI ,都自然而然的采用的是声明式编程这种形式。

推出 Flutter 的意义是一种跨平台方案,而在 Android 当前 XML-UI 编程体系十分完善的情况下,而 Compose 被推出的意义又在哪呢?先给出个人不负责任的结论:就是为了在 Android 上推行声明式 UI。若仅仅为此,大费周章、费时费力的如此做何必呢?如果你试着从谷歌的角度去思考和解读,就很容易明白了。 Android 开发的趋势终将是:整个开发体系趋于统一,而以响应式编程+声明式UI为主线。假设上面猜想是正确的,那整个技术框架就很明朗了:Kotlin + LiveData + Compose + (其他 JetPack 功能性组件如 Room 等)。 一直以来 Android 的开发是很混乱的,各种 MVC、MVP、MVVM,且具体到某种架构上也是不统一的,比如你的 MVP 和我的 MVP 可能完全不同,因为大家对具体框架上的理解也是不同的。而到具体到代码上,亦是如此,你用的 findViewById()、他用的 Butterknife,她用的 Kotlin 拓展,又来个人用的 android:onClick="onClickXXX",而官方好心救火推出 ViewBinding 确更像好心办坏事火上浇油。而无论哪种方式,都多有不便,赋值时总要通过 id 和 xml 布局文件去对应,而定位错时更是麻烦,要先确定是代码的问题还是布局的问题,而代码中有可能多个地方对同一个 View 做了改变,甚至 View 可能被传到其他地方被做了改变。 JetPack 框架一方面是为开发中的一些点提供更方便更强大也更简单的解决方法;另一方面就是为了统一整个开发体系。而 Compose 是这里面最重要也是最难的一环,说难有两个方面,一个是对于谷歌来说,为此做的工作很繁重也难,一方面要寻找好的解决方案,其实完全也可以把 DataBinding 看成响应式的一种形式,但 DataBinding 没有脱离 xml 这种布局方式,为此有很多天然局限性,而为了解决这些局限性就要做出很多妥协性的方式,从而又相应的增加了使用上的难度,更不要说 DataBinding 本身也会造成一些性能上的损耗更是得不偿失。另一方面对谷歌来说推行一个全新的 UI 库也是很难的,毕竟对于开发者来说长期以来已经熟悉固有的形式,很难去接受一个不熟悉的东西,在这方面也能看出谷歌做了很大努力,在稳定版未推出之前,已经有很多布道者。现在还没有全面接触和深入研究过 Compose 的实现原理,但无论怎样就 声明式 UI 而言,在 Flutter 面前它还只是个小学生。

那到底什么是 声明式 UI,它究竟有何魅力,这个大概聊一下。 先从说一下熟悉的命令式,它是指以一条条命令的形式告诉计算机每一步要做什么,已达到想要的结果。反应到 UI 上就是,在不同的时刻通过手动的方式去调用或设置每个控件的属性做出改变来表现当前想要的状态。

val textView = findViewById<TextView>(R.id.tvHelloWorld)

textView.setTextColor(0x123456)
textView.setText(statusStr)
textView.setOnClickListener { 
    textView.setTextColor(0x654321)
    textView.setText("clicked status")
}

类似上面这样,根据场景的不同以命令的形式,给控件设置不同的属性来做出改变。对于我们长时间这样写不会感觉有什么问题,但其实会存在很多问题,繁琐且致命:

  • 学习成本高,要记住两套知识:xml 定义属性时一套,代码动态设置属性一套,因为多数情况下名字相同不会有感觉,但有时候如果不百度你很难想起来怎么动态设置一些属性(比如 margin,比如给 svg 图标换个颜色);
  • 繁琐重复且低效:xml 中控件需要在代码中 findViewById 一一对应到变量,且使用时,还要费力去找具体的控件变量名,同时控件变量可以在多个角落被改变;
  • 有致命隐患:对于控件变量来说,findViewById 的 id 可能布局中不存在,或者绑定到其他布局中的控件 id,这种错误会停留到运行到这个页面时才能暴露;同时控件变量可以在任何地方任何时候在不知情的情况下被置空而在运行时有空指针的危险。为此,有很多对运行时安全要求较高的公司,要求对控件做调用时,每次都要做判空;
  • 造成代码逻辑混乱:这么多架构就是为了解决这个问题

那 声明式 又是什么意思呢?它的思想是只描述或告诉想要的结果,然后机器自己摸索过程。反应到 UI 上就是你只需要把想要的界面给 声明 出来,而不需要手动更新。那不手动更新,那界面怎么更新呢?使用时,我们一般是根据数据来显示界面,而数据就相当于我们想要的界面声明,而机器只需要根据数据渲染出我们想要的效果就行了。而更新时,只需要改变数据就行了,改变数据就相当于改变了界面声明,而界面此时会随着数据的改变自动更新。

声明式 UI 图解
这张图很好的描述了声明式 UI 的核心思想,简单来说就是通过 state 作为入参根据已经写好的构建 func 就能得到我们想要的 UI 效果。 我们用 Flutter 代码实现一个很上面一样的界面例子:

StatusBean statusBean = ...;

@override
Widget build(BuildContext context) {
return TextButton(
    child: Text(
      "$statusBean",
      style:
          TextStyle(color: statusBean.isClick ? Colors.red : Colors.white),
    ),
    onPressed: () {
      setState(() {
        statusBean.statusStr = "clicked status";
        statusBean.isClick = true;
      });
    });
}

分析上面的代码,可以看出,界面的展示是根据数据 statusBean 的来展示的,statusBean 就相当于想要的界面声明,而点击回调里的 setState(() {} 就是改变数据后告诉程序重新绘制的,而界面的会根据数据的改变自动更新。 而从上面的代码也能看出,原来 xml 布局存在的那些问题都不存在了。而声明式在代码上更多的工作是考虑控件的拆解和拼装,还有就是状态的管理。所以从 Flutter 开源框架上就知道,多数都是在研究状态的管理的。

殊途同归

虽然 Flutter 和 Compose 的出发点完全不同,目前的目标也不相同,但它们的趋势有可能归到一个点。

先来说说它们之间有多相似,先看看刚刚说过的都采用 声明式 UI:

flutter vs compose
能看出上面的代码有多相似吗?很多控件无论从命名还是从属性都完全一样。基本上你学会其中一个,另外一个也会一多半了。

而开发语言上虽然完全不同,但 Kotlin 和 Dart 都在谷歌的掌控下,Flutter 更是左右了 Dart 的更新方向,两种在 框架语言 之间都深入融入。而两种语言之间在使用形式上也多有相似之处,Kotlin 推出的协程和 Dart 的 async-await,Kotlin 推出的 Flow 和 Dart 的事件流,在使用形式你会发现异曲同工。而今年 Dart 的更新中采用了和 Kotlin 完全一样的形式来支持了 null 安全,在使用上也是完全一样。 提到这些再聊点更题外的话,很多人一直诟病 Flutter 选择 Dart 作为开发语言,而真正了解后,你会发现这个语言不容小觑,这语言潜力上更是巨大。从上面和 Kotlin 的对比上说到的异步和事件流,这些是 Dart 语言级别提供的支持,而 Dart 虽然是单线程语言,但提供了 isolate 天然性支持并发编程。最近看到了一个关于语言测评的系列文章,感觉比平时的编程语言排行榜有价值,有兴趣的也可看一下《现代编程语言终极测评》(这里的链接是翻译后的链接,英文原文地址没去找)

而对于未来的目标,它们也肯定是一致的,Compose 的也在向跨平台方向发展,Compose for Desktop 桌面应用的 UI 开发的支持已经被推出,目前处于 Alpha 阶段。 再从技术层面去分析这个问题,Compose 不能单单看成一个 Jetpack 的一个 UI 组件库,它设计的理念和架构本身就带有跨平台支持的能力的,并且从介绍看它也是和 Flutter 一样采用的 Skia 渲染,而 Skia 正是 Flutter 能够跨平台的基石。而再从开发语言的技术层面去分析,Dart 在布局能力上自不用说,而 Kotlin DSL 是完全支持以编程方式构建图形的,通过在代码中以声明方式构建图形,这些已经在 JetPack 的 Navigation 上也得到了使用。而目前来说 Compose for Desktop 是已经推出的,而下一个我认为极有可能是 for Web,理由当然也是从 Kotlin 的语言能力分析。去研究过 Dart 的人知道,当初 Dart 的设计目的就是为了用来取代 JavaScript 的,而 Flutter for Web 是将 Dart 直接编译成了 JavaScript,界面上部分转换成标准的 HTML 标签,部分转换成通过 Canvas 绘制的自定义标签,最终构成一个 dom 树。而 Kotlin 也同样是有转换成 JavaScript 的能力的,这个也是前期 Kotlin 推广时一个喙头的。

: 这篇文章在写的当时 Compose for web 还没有推出(这篇文章初衷是准备公司内部技术文章分享的,这段时间任务比较忙就延后了),结果一周后官方就推出了 预览版,上面的文词还是当时写的时候的场景。

最后的选择

本文没有什么条理,总算也是围绕 Flutter 和 Compose 这两个当下比较热度且相似的 UI 库来说的吧。那就最后做个总结吧,无论是 Flutter 还是 Compose ,更或是其他优秀的新技术,它们都相当于我们开发中使用的武器,你只要选择适合自己的用着随手的就行。但新技术之所以能流行被大家接受,总有它的强大之处,我们自己也不能一直固守偏执于特定的框架和语言,当你还在使用小米加步枪时,别人早已经换成加特林了,想象一下。

多去尝试新的东西,不要去纠结这个新事物后期会怎么样,哪怕你后面用不到,但至少你能从新事物中学习到一些解决问题的新思路,让自己了解更多编程的思维。所以,有时候甚至可以跳出工作需要,去学一些和自己目前技术栈差别比较大的技术或语言,以冲击自己的思维禁锢。