从Windows到Web,到Android,到iOS,我们编写应用 (App) 最初的方式都是命令式的 (Imperative Style) 。但随着声明式 (Declarative Style) 的出现,情况正在发生极速的变化。我们先看看命令式和声明式分别怎么写UI。
命令式的方式是一种自然而然想到的方式,我们写代码构建整个UI (view tree),在需要更新UI时 (比如获取到网络数据后或者用户改变UI元素后) 从view tree里查找到对应的元素,再更新它。比如在Android开发中,我们通过findViewById查找到某文本元素,再给它设置显示的内容:
-
- TextView tv = findViewById(R.id.text_view);
- tv.setText("Hello World");
命令式的方式是一种最朴素的方式,它要求我们针对每一次UI的变更都亲自写代码修改view tree,且这些代码分散在各个地方。那么我们有没有方法把我们从对view tree的频繁操作中解放出来呢?有。Facebook的React框架就是一个知名的先驱。声明式的方式集中在一个地方声明UI的结构,UI的变化通过数据来驱动。我们只需要修改数据,剩下的修改view tree的工作交给框架来做。
其实声明式的UI布局方式已经有比较长的历史,下面简单介绍一些比较知名的
前端开发在React之前,比较常用的是jQuery这种命令式的框架。下面是一个jQuery的例子,用ajax发起一个网络请求,请求返回结果后在DOM树里搜索到节点 ($("#weather-temp")) ,再将结果设置给节点显示出来。
-
- $.ajax({
- url: "/api/getWeather",
- data: {
- zipcode: 97201
- },
- success: function( result ) {
- $("#weather-temp").html( "<strong>" + result + "</strong> degrees" );
- }
- });
Facebook内部从2011年开始开发FaxJS,也就是React的原型,并于2013年开源了React。下面是一个React声明式UI的例子
-
- class MarkdownEditor extends React.Component {
- constructor(props) {
- super(props);
- this.md = new Remarkable();
- this.handleChange = this.handleChange.bind(this);
- this.state = { value: 'Hello, **world**!' };
- }
-
- handleChange(e) {
- this.setState({ value: e.target.value });
- }
-
- getRawMarkup() {
- return { __html: this.md.render(this.state.value) };
- }
-
- render() {
- return (
- <div className="MarkdownEditor">
- <h3>Input</h3>
- <label htmlFor="markdown-content">
- Enter some markdown
- </label>
- <textarea
- id="markdown-content"
- onChange={this.handleChange}
- defaultValue={this.state.value}
- />
- <h3>Output</h3>
- <div
- className="content"
- dangerouslySetInnerHTML={this.getRawMarkup()}
- />
- </div>
- );
- }
- }
-
- root.render(<MarkdownEditor />);
渲染效果:

我们将某个Component的UI描述全部放在render方法里,这个Component有它自己的状态 (state)。当用户在textarea输入内容时,handleChange会被调用,它会从textarea取出当前值,赋给value这个state变量,调用setState方法告诉React框架需要更新UI,然后框架就会完成剩下的所有工作。对开发者来说非常的简单高效。
Facebook在2015年开源了React Native这个跨平台的框架,它可以让你通过前端技术栈开发Android和iOS应用。它声明UI的方式和React一模一样,如下所示
-
- import React from 'react';
- import {Text, View} from 'react-native';
- import {Header} from './Header';
- import {heading} from './Typography';
-
- const WelcomeScreen = () => (
- <View>
- <Header title="Welcome to React Native"/>
- <Text style={heading}>Step One</Text>
- <Text>
- Edit App.js to change this screen and turn it
- into your app.
- </Text>
- <Text style={heading}>See Your Changes</Text>
- <Text>
- Press Cmd + R inside the simulator to reload
- your app’s code.
- </Text>
- <Text style={heading}>Debug</Text>
- <Text>
- Press Cmd + M or Shake your device to open the
- React Native Debug Menu.
- </Text>
- <Text style={heading}>Learn</Text>
- <Text>
- Read the docs to discover what to do next:
- </Text>
- </View>
- );
Facebook在2017年开源了Litho这个高性能的Android UI开发框架,在开源之前已在内部主要App广泛使用,如Facebook等。受React的影响,它也采用了声明式的UI布局方式。下面是一个例子
- class PostStyledKComponent(val post: Post) : KComponent() {
- override fun ComponentScope.render(): Component {
- return Column {
- child(
- Row(alignItems = YogaAlign.CENTER, style = Style.padding(all = 8.dp)) {
- child(
- Image(
- drawable = drawableRes(post.user.avatarRes),
- style = Style.width(36.dp).height(36.dp).margin(start = 4.dp, end = 8.dp)))
- child(Text(text = post.user.username, textStyle = Typeface.BOLD))
- })
- child(
- Image(
- drawable = drawableRes(post.imageRes),
- scaleType = ImageView.ScaleType.CENTER_CROP,
- style = Style.aspectRatio(1f)))
- }
- }
- }
渲染效果:

Google在2018年发布了Flutter的第一个稳定版本v1.0.0。它采用了声明式的布局方式,下面是一个简单的例子
- class MyApp extends StatelessWidget {
- const MyApp({super.key});
-
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Welcome to Flutter',
- home: Scaffold(
- appBar: AppBar(
- title: const Text('Welcome to Flutter'),
- ),
- body: const Center(
- child: Text('Hello World'),
- ),
- ),
- );
- }
- }
Apple在2019年发布了SwiftUI的第一个版本,它让你用声明式的方式来写iOS应用。下面是一个简单的例子
- import SwiftUI
-
- struct AlbumDetail: View {
- var album: Album
-
- var body: some View {
- List(album.songs) { song in
- HStack {
- Image(album.cover)
- VStack(alignment: .leading) {
- Text(song.title)
- Text(song.artist.name)
- .foregroundStyle(.secondary)
- }
- }
- }
- }
- }
Google在2021年7月发布了Jetpack Compose的第一个稳定版本v1.0,像Flutter一样采用声明式UI布局方式,但Compose的设计比Flutter更合理一些。在Flutter里,你要给一个Widget设置宽高等属性都需要在外面套一层Container来实现,这非常的臃肿,而其它声明式的框架,包括Compose,大都可以通过节点的属性来设置。下面是一个Compose的简单例子
- import androidx.compose.foundation.layout.Spacer
- import androidx.compose.foundation.layout.height
- import androidx.compose.foundation.layout.padding
- import androidx.compose.foundation.layout.size
- import androidx.compose.foundation.layout.width
- import androidx.compose.foundation.shape.CircleShape
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.draw.clip
- import androidx.compose.ui.unit.dp
-
- @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)
- }
- }
- }
渲染效果:

我们看到2017年的Litho,2018年的Flutter,2019年的SwiftUI,2021年的Jetpack Compose都采用了声明式的UI布局方式,已呈燎原之势,声明式的UI布局方式未来会像前端领域的React一样成为主流吗?