2022年了,再来谈谈 App UI开发
前端快速发展了这么多年,诞生了非常多有意思的 State 框架,用法上都有自己的一套,但是归根结底还是围绕着 Mutable 还是 Immutable 的选择。本人深受 React 的颠覆式影响,近几年都是跟随 Immutable 的单项数据流模式,所以在架构上选择时,除了基础的 MVVM + Reactive,有时还会把 Flux 也引入其中。
说到 Flux,那就不得不提 Redux 了。Redux 并不是完全照搬了 Flux 的思想,而是提倡全局 Store,整个应用使用同一个 Store,所有的公共状态都从 Store 获取。这当然是有待商榷的,因为应用大了,公共状态必然很多,怎么分别管理子状态是要慎重考虑的。本人更倾向于尽量使用类似 React 的 useState
,先把状态分散化管理,有需要的复杂界面采用useReducer
, 全局 Store 只存储一些登录状态、主题、语言等相关的设置。
State 设计好之后,就是 UI 开发的事情了。上古时代,jQuery 将 State 和 CSS 一起拼成 HTML 字符串给某个标签赋值,对应到前几年写的 iOS UIKit 代码也是类似情况,那种命令式刷新 UI 的方式难看至极,已经过时了。
所以 React Native 和 Flutter 诞生了,这两者出生的时候都获得了很大关注。JSX 和 Widget 代码类似,都是把样式和布局写在一起,虽然被 Web 开发人员唾弃,说它们应该像网页一样分成 CSS 和 HTML,不然就会地狱嵌套了。我只能说这种吐槽是图样图森破,证明开发人员没学到位。
先复习一下 React Native 的写法:
import React from 'react';
import { View, Text, Image, ScrollView, TextInput } from 'react-native';
const App = () => {
return (
<ScrollView>
<Text>Some text</Text>
<View>
<Text>Some more text</Text>
<Image
source={{
uri: 'https://reactnative.dev/docs/assets/p_cat2.png',
}}
style={{ width: 200, height: 200 }}
/>
</View>
<TextInput
style={{
height: 40,
borderColor: 'gray',
borderWidth: 1,
}}
defaultValue='You can type in me'
/>
</ScrollView>
);
};
再对比一下 Flutter 的 demo 例子:
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(bottom: 8),
child: const Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
],
),
);
我是接受并喜欢这种方式的,因为这就是我想要的开发模式,即声明式(Declarative)编程。
声明式编程是一个大的概念,除了 SQL、Make 编译、正则表达式、HTML 等一些特定的领域专属语言之外,一些有名的编程范型也被归类为其子范型,比如函数式编程,尤其是纯函数式编程。与其相对的是命令式编程,包括 C/C++/Objective-C,Java 等。当然还有一些语言是两者都支持的,因为语言标准还在改进,年龄大的比如 C# 3.0+、Java 8+、JavaScript ES2015+,年轻的 Swift、Kotlin、Dart 等。
函数式编程,可以说是我这几年发现的宝藏,虽然它已经出现几十年了,但近 10 年才得到比较蓬勃的发展,应该归功于 JavaScript 的新标准快速发布。为什么这么流行肯定是有原因的,我们以 Javascript 举例来说明函数式编程的好处:
// 1
var array = [1, 2, 3];
var output = [];
for (var i = 0; i < array.length; i++) {
var tmp = array[i] * 2;
output.push(tmp);
}
console.log(output);
// 2
output = array.map(function (n) {
return n * 2;
});
console.log(output);
// 3
let doubleTimes = (n) => n * 2;
output = array.map(doubleTimes);
console.log(output);
上面三个 console 打印都是一样的结果,第一个是命令式的编程格式,第二个是直接在map
里面写匿名逻辑函数, 第三个是使用纯函数当参数。
纯函数是没有副作用的,比如 doubleTimes 函数。这里的副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响,例如修改全局变量(函数外的变量)或修改参数(C++没有加 const 的引用类型参数)。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。这个概念对刚接触函数式编程的开发人员有点难以理解,其实看多了就懂。
通过将每一个小功能都拆分成一个个的纯函数,我们就可以通过链式调用的方式(类似 Promise)组合起来,达成一个复杂的功能,比如:
Maybe.of(null).chain(safeProp('address')).chain(safeProp('street'));
正因为函数式没有副作用,我们也可以使用函数式编程的方式来开发 UI。当参数不变时,即状态不变时,UI 是没有变化的,可以用公式 UI = f(State) 来表示。
不同的函数可以代表不同的 UI 组件,复杂的界面通过组合不同的函数来绘制,即 SwiftUI 这种。当然不同的平台会有不一样的实现方式,但是这种组合(Composable)的概念是相通的,如 Web Components 可以 自定义 elements 和 template。
我们把这类 UI 开发范式取个名字,即声明式 UI 开发,这种编程方式采用更接近自然语义,让开发者可以直观地描述 UI 界面,不必关心框架如何实现 UI 绘制和渲染,从而实现极简高效的开发。
现代或将来的开发 App 的 UI 框架必定是走这个方向的,不要 xml 或 storyboard 布局,纯代码写 UI,并且能够 Hot Reload。以 iOS 的 SwiftUI 和 Android 的 Jetpack Compose 为例,Apple 和 Google 都是不余遗力的各自推广自家的新框架。接下来我们来比较一下 SwiftUI、Jetpack Compose 的写法:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
}
}
@Composable
fun MessageCard(msg: Message) {
// Add padding around our message
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = "Contact profile picture",
modifier = Modifier
// Set image size to 40 dp
.size(40.dp)
// Clip image to be shaped as a circle
.clip(CircleShape)
)
// Add a horizontal space between the image and the column
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(text = msg.author)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(4.dp))
Text(text = msg.body)
}
}
}
可以看得出来,这两者基本上没有差别吧,熟悉其中一种,立马可以上手另一种。
另外,国内的华为 HarmonyOS 也支持多语言开发 UI,有 Java UI、JS UI 两种。其中 Java UI 没有创新, 跟 Java Android 是一样的开发模式,JS 开发也就是类似 Web 和小程序开发一样,分成 HTML、CSS、JavaScript 三块。但是 HarmonyOS 在 2021 年 10 月发布 3.0 Beta 版本,JS UI 有了大动作,首先将名字正式命名为 ArkUI,JS 新增 5800+的 API 接口,重点是扩展了另一套极简声明式 UI 范式 eTS,这是基于 Typescript 的声明式开发范式。在最新的 DevEco Studio 创建新工程时,就有三个选项了。
eTS 是我们关注的重点,下面我们来看看官方 Demo 的效果:
import { TopTabs } from '../common/topTabs'
import { SubscribeSwiper } from '../common/subscribe'
import { HomeListItem } from '../common/homeListItem'
@Component
export struct HomeTabComponent {
private recommends: Resource[] = [$r('app.string.today_recommend'),
$r('app.string.featured_recommend'),
$r('app.string.september_recommend'),
$r('app.string.august_recommend')]
@Link showSettings: boolean
build() {
Column() {
TopTabs({ showSettings: $showSettings })
List() {
ListItem() {
SubscribeSwiper()
}
ForEach(this.recommends, item => { // ForEach语句来显示推荐列表
ListItem() {
HomeListItem({ titleIndex: this.recommends.indexOf(item) }) // 推荐列表Item,使用自定义组件HomeListItem
}
}, item => this.recommends.indexOf(item).toString())
}
.listDirection(Axis.Vertical)
.width('100%')
.layoutWeight(1)
}
}
}
我在官网看完这个 Demo 之后,顿时感觉 App UI 开发范式有大一统的感觉了。
最后我想说一下,声明式开发是整个大前端的趋势,尤其是在 UI 这一块,但是这个趋势不代表命令式开发就没有存在的必要,毕竟还有这么多老项目以及这么多不愿意尝试的老人。这里说的老人不一定是年龄大的程序员,还有些思想顽固的意思。如果你有所追求,就打开 IDE 开干吧!