為什麼說 Compose 的宣告式程式碼最簡潔 ?Compose/React/Flutter/SwiftUI 語法對比
highlight: androidstudio theme: simplicity-green
前言
Comopse 與 React、Flutter、SwiftUI 同屬宣告式 UI 框架,有著相同的設計理念和相似的實現原理,但是 Compose 的 API 設計要更加簡潔。本文就這幾個框架在程式碼上做一個對比,感受一下 Compose 超高的程式碼效率。
1.Stateless 元件
宣告式 UI 的基本特點是基於可複用的元件來構建檢視,宣告式 UI 的開發過程本質上就是各種 UI 元件的定義過程。元件在型別上一般分為無狀態的 Stateless 元件和有狀態的 Stateful 元件。
React 提供了類元件,函式式元件兩種元件定義方式:
js
//JS
function Greeting(props) {
return <p>Hello, {props.name}</p>;
}
js
//JS
class Greeting extends React.Component {
render() {
return <p>Hello, {this.props.name}</p>;
}
}
函式元件的資料通過 JS 函式引數傳遞;類元件通過 JSX 的標籤屬性設定,並通過 Class 的 this.props
讀取。注意 props
不同於 state
, 它是隻讀的不可變化,這也是 Stateless 和 Stateful 的本質區別。在程式碼上函式式元件更加簡潔,避免了類定義帶來的模板程式碼,因此,函式式元件在 React 中的使用佔比越來越高。
類元件和函式元件也將宣告式 UI 框架劃分為兩個流派,Flutter 和 SwiftUI 屬於前者,而 Compose 屬於後者。這也從基礎上決定了 Compose 的元件的定義將更加簡潔。
讓我們分別看一下 Flutter 和 SwiftUI 的 Stateless 元件
```js //Dart class Greeting extends StatelessWidget { const Greeting({required this.name});
final String name;
@override Widget build(BuildContext context) { return Text("Hello, $name"); } } ```
Flutter 使用類元件繼承的特性,通過 StatelessWidget 派生自定義 Stateless,然後定義建構函式用來傳遞資料。build(BuildContext)
方法中通過例項化子元件並構建 UI 。這還得感謝 Dart2 中對 new
關鍵字可以省略,不然建構函式的呼叫程式碼會更顯臃腫。
```swift //Swift struct Greeting: View { var name: String
var body: some View {
Text("Hello, \(name)")
}
} ```
嚴謹地說 SwiftUI 元件不是類元件而是”結構體元件”。Class 是引用型別,而 Struct 是值型別。使用結構體定義元件有助於提升 UI 的不可變性,也是從面向物件向函數語言程式設計過度的一種體現,但是結構體元件從形式上更接近類元件,不如函式元件簡潔。
接下來看一下 Compose 的 Stateless:
kotlin
//Kotlin
@Composable
fun Greeting(name: String) {
Text("Hello $name")
}
Compose 的程式碼明顯更簡潔,幾乎就是一個普通的函式定義,唯一的區別就是增加了一個 @Composable 註解,這個註解在編譯期生成許多輔助框架執行的程式碼,開發者可以少些很多程式碼,從程式碼量的角度來看,次註解的價效比非常高。
即使同為函式元件的 React 相比,Compose 也更勝一籌,Composable 無論定義還是使用都是基於 Kotlin,使用體驗更一致。而 React 的函式式元件需要在 JSX 中使用,雖然符合前端開發習慣,但是但從程式碼複雜度上來說是不友好的。另外 Composable 沒有返回值,連 return
都省了,更簡潔。
|Compose| React|
|:--:|:--:|
||
|
2.Stateful 元件
React 的函式式元件使用 Hooks API 定義狀態
js
//JS
function Counter() {
const [count, setCount] = useState(0);
return (
<><button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
React Hooks 開創了宣告式 UI 狀態管理的新方式,相對於傳統的基於父類方法的方式程式碼效率得到大幅提升。Compose 的狀態管理以及各種副作用 API 的設計靈感也來自 React Hooks. (參考:相似度99%?Jetpack Compose 與 React Hooks API對比)
Flutter 中自定義 Stateful 元件是比較繁瑣的,首先 StatefulWidget 返回一個 State d物件,Widget 定義在 State 中。
```js //Dart class Counter extends StatefulWidget { @override _Counter createState() => _Counter(); }
class _Counter extends State
void _incrementCounter() { setState(() { _counter++; }); }
@override Widget build(BuildContext context) { return TextButton( onPressed: _incrementCounter, child: Text("$_counter"), ); } } ```
State 的變化觸發 Widget 的重新構建,這確實貫徹了狀態驅動 UI 的設計原則,但是增加了心智理解的成本。當然,也有諸如 Flutter Hooks 這樣的三方庫可供使用,實現類似 React Hooks 的效果:
```js //Dart class Counter extends HookWidget { @override Widget build(BuildContext context) { final counter = useState(0);
return TextButton(
onPressed: () => counter.value++,
child: Text("${counter.value}"),
);
} } ```
SwiftUI 的 Stateful 的定義比較簡潔:
```swift //Swift struct Counter: View { @State var count = 0
var body: some View {
Button(
action: { count += 1 },
label: { Text("\(count)")}
)
}
} ```
使用 @State
註解定義一個成員變數,變數的變化可以自動觸發介面重新整理。
最後看一下 Compose 的 Stateful:
kotlin
//Kotlin
@Composable
fun Counter() {
val count by remember { mutableStateOf(0) }
Button(
onClick = { count++ }
) {
Text("${count}")
}
}
Compose 的 remember
本質也是一種 Hooks 函式,但是 Compose 的 Hooks 是用起來比起 React 更方便,在 React 中是用 Hooks 有諸多限制,例如下面這些用法都是不允許的。
- 將 Hooks 函式放在條件分支裡
js
//JS
if (flag) {
const [count, setCount] = useState(0);
...
}
在子元件定義時,使用 Hooks 函式
```js
//JS
return (
) ``` 在 Composable 中這些都不是問題,因為 Compose 獨有的 Positional Memoization 機制,可以根據靜態的程式碼位置儲存狀態,不會受到執行時的分支條件變化的影響。另外 Compose 所有程式碼都是同構的,不會存在 JSX 無法插入 Hooks 的窘境,所以上面兩種 React 中的禁忌在 Compose 中都可以實現:
kotlin
//Kotlin
if (flag) {
val count by remember { mutableStateOf(0) }
...
}
kotlin
//Kotlin
Column {
val count by remember { mutableStateOf(0) }
...
}
3. 控制流語句
我們經常有根據分支條件顯示不同元件的需求,那麼各個框架是如何在宣告式語法中中如何融入 if/for
等控制流語句的呢?
Compose 的函式式元件在這方面有天然優勢,構建 UI 的本質就是一個函式實現的過程,過程中可以自然地插入控制流語句
kotlin
//Kotlin
@Composable
fun List(value: List<Data>) {
Column {
Header()
if (value.isEmpty()) {
Empty()
} else {
value.forEach {
Item(it)
}
}
}
}
上面的 Compose 例子中,通過 if..else
顯示不同結果,當資料不為空時,使用 for
迴圈依次展示,程式碼非常直觀。
反觀 Flutter ,基於類元件的宣告式 UI 本質上是不斷構建物件的過程子元件通過構造引數傳入,這個工程中插入控制流會比較複雜,上面同樣的 UI 在 Flutter 中寫會像下面這樣:
js
//Dart
@override
Widget build(BuildContext context) {
List<Widget> widget;
if (value.isEmtpy) {
widget = Empty();
} else {
for (var i in value) {
widget.add(i);
}
}
return Column(children: [
Header(),
...widget
]);
}
所幸,Dart 2.3 之後新增了 Collection-if
和 Collection-for
,可以在 List 構造中使用 if/for
,程式碼大大簡化:
js
//Dart
@override
Widget build(BuildContext context) {
return Column(children: [
Header(),
if (value.isEmpty) Empty(),
for (var i in value) Item(i)
]);
}
SwiftUI 原本應該像類元件那樣通過對 Struct 的初始化新增子元件,但是它提供了 ViewBuilder
這樣的機制,可以使用 DSL 進行 UI 構建,和 Compose 幾乎無異
swift
//Swift
var body: some View {
VStack {
Header()
if value.isEmpty {
Empty()
}
ForEach(value) {
item inItem(item)
}
}
}
需要注意 ViewBuilder 中不能使用普通的控制流語句,ForEach
是針對 SwiftUI 定製的方法。
無論是 Flutter 還是 SwiftUI 他們的控制流語句都需要依賴一些定製語法或者語法糖,不像 Compose 那樣樸實,程式碼的可複用性也自然會受到影響。
最後簡單看一下 React 吧,同樣的邏輯實現如下
js
//JS
function List(value) {
return (
<div><Header />
{ value.isEmpty() && <Empty /> }
{ value.map((item) => <Item value={item} />) }
</div>
)
}
雖說是函式元件,但是新增子元件的邏輯不能用純 JS 實現,需要在 JSX 定義,幸好 JSX 對這樣的控制流邏輯也有一些支援。
4. 生命週期
宣告式 UI 中都針對元件在檢視樹上的掛載/解除安裝定義了生命週期,並提供了響應 API。
React 類元件通過類的成員方法提供生命週期回撥,我們重點看一下函式元件的生命週期回撥
js
//JS
useEffect(() => {
const callback = new Callback()
callback.register()
return () => {
callback.unregister()
};
}, []);
useEffect
也是一種 Hooks 函式,我們可以利用它監聽元件的生命週期。最後返回的 lambda 是可以作為元件解除安裝時的回撥。
Compose 參考 useEffect 提供了一系列副作用 API,以 DisposableEffect
為例
kotlin
//Kotlin
DisposableEffect(Unit) {
val callback = Callback()
callback.register()
onDispose {
callback.unregister()
}
}
設計上完全致敬 Hooks,最後 onDispose
是 Composable 從 Composition 中退出時的回撥。
Flutter 作為類元件,自然是通過繼承自父類的方法回撥生命週期
```js //Dart class Sample extends StatefulWidget { @override _State createState() { return _State(); } }
class _State extends State
@override Widget build(BuildContext context) { return ...; }
@overridevoid initState() { super.initState(); callback.register() }
@overridevoid dispose() { super.dispose(); callback.unregister() } } ```
當然,使用前面提到的 Flutter Hooks 的話,可以達到 React 與 Compose 的效果。
SwiftUI 的結構體元件沒有繼承,所以通過 onAppear
和 onDisappear
設定生命週期回撥。相對於繼承的方式更加簡潔,但是它只能設定子元件的回撥,無法對當前元件進行設定。
```swift //Swift struct Sample: View { private let callback = Callback()
var body: some View {
Component()
.onAppear(perform: {
callback.register()
})
.onDisappear(perform: {
callback.unregister()
})
}
} ```
綜上,React 和 Compose 的 Hooks 風格的生命週期回撥最為簡潔,因為掛載/解除安裝的回撥可以在一個函式中完成,例如當我們要往一個 callback
例項上註冊/登出回撥時,可以閉環完成操作,不必額外儲存這個 callback 例項。
5. 裝飾/樣式
對比一下元件樣式的設定上 API 的區別,以最常用的 background
,padding
等為例。
React 基於 JSX 和 CSS-in-JS
,可以像寫 HTML + CSS
那樣設定元件樣式,可以比較好地實現 Style 與 Component 的解耦
js
//JS
const divStyle = {
padding: '10px',
backgroundColor: 'red',
};
return <div style={divStyle}>Hello World</div>;
Compose 通過 Modifier 為 Composable 設定樣式
kotlin
//Kotlin
Text(
text = "Hello World",
modifier = Modifier
.background(Color.Red)
.padding(10.dp)
)
Flutter 通過 Widget 的構造引數設定樣式,使用比較簡單,但是不具備 Modifier 的靈活性,不同 Widget 的 Style 無法複用。
js
//Dart
Container(
color: Colors.red,
padding: const EdgeInsets.all(10),
child: Text("Hello World"),
)
SwiftUI 的樣式設定是基於元件例項的鏈式呼叫,非常簡單
swift
//Swift
Text("Hello World")
.padding(10)
.background(Color.red)
綜上,在樣式設定上各家的 API 風格都比較簡單,但是 Compose 的 Modifier 仍然具有不可比擬的優勢,比如型別安全和容易複用等,Modifier 本身也是一種非常好的設計模式。
總結
前面基於程式碼片段進行了一些對比,最後以 Counter Demo 為例,看一個完整功能下 Flutter、Compose 和 Swift 的程式碼對比,React 與其他三者程式碼風格差異較大,就不參加比較了。
|Flutter|Compose|SwiftUI|
|:--:|:--:|:--:|
||
|
|
可以感覺到 Compose 程式碼最簡潔也最直觀,SwiftUI 通過 ViewBuilder 機制也可以實現與 Compose 類似的 DSL,表現也非常不錯,Flutter 由於模板程式碼較多,在簡潔程度上表現最差。
Kotlin、Dart 和 Swift 的語法非常相近,所以拋開語言層面的差異,Compose 的優勢主要還是來自於其採用了函式式的元件形式並借鑑了 React Hooks 的設計思想。可以說 Compose 誕生於 React 的肩膀上,並藉助 Kotlin 將程式碼效率提升到一個新高度。
我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。
- Android Studio Electric Eel 起支援手機投屏
- Compose 為什麼可以跨平臺?
- 一看就懂!圖解 Kotlin SharedFlow 快取系統
- 深入淺出 Compose Compiler(2) 編譯器前端檢查
- 深入淺出 Compose Compiler(1) Kotlin Compiler & KCP
- Jetpack MVVM七宗罪之三:在 onViewCreated 中載入資料
- 為什麼說 Compose 的宣告式程式碼最簡潔 ?Compose/React/Flutter/SwiftUI 語法對比
- Compose 型別穩定性註解:@Stable & @Immutable
- Fragment 這些 API 已廢棄,你還在使用嗎?
- 告別KAPT!使用 KSP 為 Kotlin 編譯提速
- 探索 Jetpack Compose 核心:深入 SlotTable 系統
- 盤點 Material Design 3 帶來的新變化
- Compose 動畫邊學邊做 - 夏日彩虹
- Google I/O :Android Jetpack 最新變化(二) Performance
- Google I/O :Android Jetpack 最新變化(一) Architecture
- Google I/O :Android Jetpack 最新變化(四)Compose
- Google I/O :Android Jetpack 最新變化(三)UI
- 一文看懂 Jetpack Compose 快照系統
- 聊聊 Kotlin 代理的“缺陷”與應對
- AAB 扶正!APK 再見!