GetX的非议与所谓声明式UI

Posted by lx8421bcd on December 4, 2023

前言

数据源 - 数据处理 - 数据展示,时如何组织代码安排好这三者的关系,是所有客户端开发都不能回避的问题。在Android/iOS中,这个话题叫架构,在Flutter里,这个话题叫状态管理。

我是在做Flutter的技术选型的时候才关注到,然后很奇妙的,怎么感觉这个状态管理好像是个Flutter技术圈经久不衰的撕逼话题?Android架构选型虽然也经常撕逼,但Android架构基本上是几年一个潮流。我到今年干了整10年的Android,也算是见证了一下Android应用架构从MVC(-2014)到MVP(2015-2017)再到MVVM(2017-)的过程(这两年大厂又提出什么MVI,就我个人感觉,比MVVM优化不多,不一定能流行起来)

但Flutter的状态管理似乎从Flutter1.0一直撕到了3.X,201X年的文章在撕,到了202X年的文章还在撕。做技术选型的时候一看,给我这个对Flutter没什么大项目实操经验的人给整不会了……

一会是bloc事件驱动灵活,一会是Provider官方支持小巧易懂,一会是GetX一把梭,而且我发现GetX的话题性尤其大,支持者和反对者都很激进,反对者说GetX的代码风格相比于「flutter原本的风格」变化太大,容易变成面向GetX编程,离了GetX不会写Flutter代码;支持者说,我管你什么狗屁风格,我就想方便好用,看得懂,早点下班。

研究了一整天,撕逼文看了无数,无解。那就主流的几个状态管理框架各写一个demo试试看吧。还别说,这一试,一下就解决了我的选择困难症。顺便也算是基本理解了这个话题经久不衰的原因。

先说一下技术选型的结论,毫无疑问直接选GetX,而且我的看法是在有GetX提供了从简单到复杂的UI构建都有解决方法的一条龙服务的情况下,没有任何理由选择其他框架给自己找不痛快。至于撕逼,我的结论,可能比较暴论,一句话概括:

之所以Flutter状态管理撕逼经久不衰,是因为当前这个所谓的「声明式UI构建」潮流,本身就是歪门邪道。

如果你对状态管理的研究到了发现「声明式」与「命令式」这两个关键词,恭喜你,发现了这个这个状态管理撕逼的关键点。即本质上,什么叫“Flutter Style”?然后你试图研究这两个词的含义,你会发现,这特娘的是个营销号水文的重灾区。

要么是上来就是贴个Hello World都不如的东西,然后不知怎么的得出结论,然后开始自说自话:诺,这是命令式UI写法,这是声明式UI写法,看,声明式比命令式更直观,命令式马上就要被淘汰啦巴拉巴拉。有效信息含量之低令人发指……

更有老哥,直接来一句,native开发是命令式的,flutter开发是声明式的,命令式的马上就要淘汰了,赶快学flutter,不然也要被淘汰了,让人看后不禁直呼哪来的弱智?

「命令式」&「声明式」

本文就不从什么历史经纬和编程语言设计的角度去说什么是「命令式」什么是「声明式」编程了,太绕。简单来说,可以这么理解:

  • 「命令式编程」强调的是“我,想「让」机器「去做」什么”。
  • 「声明式编程」强调的是“我,「需要」机器「为我做」什么”

换句话说,命令式编程就是其字面意义,你的代码是在给机器下达指令,每一行代码都是独立的,输入了什么,输出了什么,改变了什么,都是明确的。典型如汇编,你要是写汇编那当然是纯的不能再纯的「命令式」了。

而声明式编程,更加强调“你的需求”,你的代码是你需求的表达,是被机器当成一个整体来执行的,单行代码可能没有什么意义,甚至没有执行能力。从某种角度来说更像是在写“配置文件”,或者说“配置文件”就是典型的声明式编程(只不过为了简单方便不用编程语言来写)。

大概理解这点,你就会发现,营销号们在不断复读的“声明式编程”更“高级”,从某种角度来说其实是有一点道理的,在编程语言领域,越是高级语言就越倾向于逻辑表达而非控制机器,忽略机器执行的细节,这就是高级语言存在的目的。

但后者高级不等于前者淘汰了。很简单的一个道理,“你的需求”谁来实现呢?从底层的机器码,到汇编,再到C语言等高级语言,再到抽象程度更高更高级的脚本语言,表达“你的需求”的代码的实现细节,并不是像神灯实现你愿望一样虚空变出来的,而是被这些语言给“封装”,包办了。那是怎么给你包办的?编译成低级的,你不好看懂但机器好看懂的「命令式」的语言。

另一方面,在高级语言编写业务的过程中,所谓「声明式」与「命令式」的界限其实是很模糊的。大部分时候,你以为的「命令」其实也是一种包含了无数指令的「声明」。《深入理解计算机系统》第一章就用了大量的篇幅给你讲解了你在C语言中一个简单的“printf()”背后经历了多少步骤与指令,才将一个简单的“Hello World”字符串输出到屏幕上。这个时候问题就来了,“printf("Hello World");”这简简单单的一行代码,到底是在给机器「下命令」「输出“Hello World”」呢?还是给机器「描述需求」「我要在Console上显示“Hello World”」呢?

简而言之一句话,所谓「声明式」与「命令式」编程,前者是后者的抽象,后者实现前者的需求,各有各的适用场景,并不存在谁替代谁。

所谓「声明式UI」构建

把上面的对「声明式」与「命令式」的理解放到“UI构建”这个话题上,就有些微妙了。营销号们是怎么说「声明式UI」和「命令式UI」的?拿一个按钮点击变颜色举例

这是所谓命令式(Android - kotlin)

val viewA = View()
viewA.setWidth(100)
viewA.setHeight(100)
viewA.setBackgroundColor(R.color.black)
viewA.setOnClickListener { view ->
    view.setBackgroundColor(R.color.grey)
}
parent.addView(viewA)

这是所谓声明式(Flutter - dart)

Color btnColor = Colors.black;

@override
Widget build(BuildContext context) {
    return Container(
        child: FilledButton(
            style: FilledButton.styleFrom(
                backgroundColor: btnColor,
            ),
            onPressed: () {
                setState({
                    btnColor = Colors.grey;
                });
            }
        ),
    );
}

简而言之,营销号嘴里的「命令式」UI构建,是View要一个一个new,属性要一个一个填,布局要一个一个add。而「声明式」是一口气就把UI层级、属性,操作逻辑都写好,UI构建只与当前的状态变量相关,UI操作只能是“触发状态变化”,而不是「指定某个UI改变什么属性」。

那么,“微妙”的点在哪呢?

首先,按照上面这种定义,Android使用XML编写layout布局,不就是经典的不能再经典的「声明式」UI构建嘛?怎么整个native开发就成「命令式」的了呢?

其次,怎么就得出“声明式更高级,更方便,必将逐步取代命令式UI构建”的结论了呢?

且不说上面以Android XML为例的Native开发实践经验来看,「声明式」和「命令式」本来就没有任何冲突,就说在我们实现业务需求时候,在语义上,本来就存在着大量的「命令」。是的,构建UI的的时候,「声明式」非常方便,没谁愿意一个一个new,一个一个add。但到了构建完成,控制UI的时候,「命令式」天然符合我们的需求。

在很多业务上,我们的需求的语义是「LoadingView,显示加载」而非「首页,加载数据了,更新」,是「弹幕列表,增加一条弹幕」而非「直播间,弹幕列表变了,更新」。

我们的需求是明确的,具体的,就是要精确控制某一个View改变它的属性。

为什么我前文说,所谓的「声明式UI构建」潮流是歪门邪道?

这句暴论,当然并不是指提出和编写诸如flutter、Android Jetpack Compose之类的声明式UI构建框架本身歪门邪道。其实如果用过这些框架就会明白,与其说他们是在追求所谓「声明式构建UI」,不如说他们是在追求「All in source code」,通俗点说就是”代码就能实现的东西,写配置文件绕一圈干什么?”。而所谓的「声明式UI构建」只不过是在「一切皆源码」这个追求下,UI布局实现上的必然选择而已,毕竟体验过声明式构建布局的直观爽快之后,换谁都不想通过一个一个new一个一个add来写UI布局,对吧?

但很多营销号,框架使用者,就有点走火入魔,刻意追求一切皆声明式,为了保持这种”声明式的风格”,排斥一切「命令式」调用,自己给自己戴脚镣,自己给自己造不必要的麻烦,给人的直观感觉,就是脱了裤子放屁。

一行「命令式」代码能解决的事,非要定义一一个状态,让View根据这个状态构建,然后改变这个状态,再用某种机制触发View重建,让View根据状态的更新来更新,绕这么大一个圈脱了裤子放屁,图啥?仅仅是因为这样写很有「声明式」的范,很潮吗?

小结 - 撕逼点在哪?

把这个营销号群魔乱舞的「声明式UI」与「命令式UI」的概念理顺,再把目光看回Flutter,就很容易理解为啥状态管理的撕逼经久不衰。所有Flutter状态管理框架出现的原因,都是一个:

Flutter原生的UI更新方式,无法满足复杂工程业务需求

要说「声明式」,Flutter原生的UI更新方式 setState()就已经是纯的不能再纯的「声明式」了。对于UI你啥都不用管,Flutter本身的Widget构建方式也决定了你啥都不能管。按照官方的想定,会出现UI刷新的页面你就应该定义为一个 StatefulWidget,在数据源产生变化时直接 setState({})通知UI重建,而重建时用的数据源就是更新后的数据源。这样你就只用写UI构建的代码,而不用写一堆 xXX.xxxx()的「命令式」代码。

多么简洁~ 多么轻松~ 可读性多么高~ 维护起来多么简单~

但怎么大家在正式写项目时都不这么干呢?为啥还要造一堆状态管理框架?

那当然是复杂的商业项目无论从性能上,交互体验上,都不允许你“整页整页”的刷新UI啊!真要像上面说直播间的那样,来一条弹幕你就整个直播间 setState({})一下,人气高点的直播间或者有人发个抽奖刷弹幕你不直接爆炸了?

那有朋友有要说了,setState()又不是不支持局部刷新,你把你要局部刷新的部分(比如上面说的弹幕列表)单独写成一个 StatefulWidget,然后根据数据源(比如弹幕列表)的状态自行更新不就行了?说得好,没错,是可以这么干的。

但怎么大家在正式写项目时都不这么干呢?为啥还要造一堆状态管理框架?

就问你,换你愿意有多少局部刷新UI的需求就写多少自定义控件吗?就算你是任劳任怨老黄牛,你就按这个方法硬造,每一个页面,都带着几个乃至几十个自定义Widget,这代码的可读性和可维护性,不说是一坨答辩,也可称一堆垃圾了。

所以,为什么大家要造一堆状态管理框架?本质上就一句话概括:

在声明式UI的大框架下,以相对命令式的方式实现(局部)UI更新

“声明式UI的大框架”没什么好说的,Flutter本身的机制决定了你只能以重建UI的方式刷新UI状态,那为什么说是“以相对命令式”的方式呢,因为所有主流的状态管理框架,其实现的根本逻辑都是“在需要实时更新状态的Widget外面套一层专用于监听数据源的Widget,并通过这个Widget在数据源更新时重建需要更新的UI Widget”,追到最后,你会发现,实际上你仍然是在通过某种方式给这个”外套”下「命令」改变UI。

明白这一点,你就会发现争论所谓”BLOC、Provider、GetX”,哪个更纯正,哪个更Flutter Style根本毫无意义,狭义来说大家都不是Flutter Style。你要想写纯的Flutter Style,那你去写 setState()吧;从广义上来说,大家都是Flutter Style,因为大家刷新UI的机制本质上和你自己套一层去 stateState()没有什么区别,只不过这些框架帮你把“套一层”这个操作给包办了而已。

而GetX之所以受追捧,只不过是因为它在实现“套一层”这个事上,做的最简单,最易懂,最符合大家对于「命令式」调用的直觉而已。

所以,选择GetX做状态管理需要有什么心理负担吗?担心自己用了GetX,就只会GetX不会Flutter了?大可不必,用就完事了。