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 的核心思想,簡單來說就是通過 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:
能看出上面的程式碼有多相似嗎?很多控制元件無論從命名還是從屬性都完全一樣。基本上你學會其中一個,另外一個也會一多半了。
而開發語言上雖然完全不同,但 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 推廣時一個喙頭的。
最後的選擇
本文沒有什麼條理,總算也是圍繞 Flutter 和 Compose 這兩個當下比較熱度且相似的 UI 庫來說的吧。那就最後做個總結吧,無論是 Flutter 還是 Compose ,更或是其他優秀的新技術,它們都相當於我們開發中使用的武器,你只要選擇適合自己的用著隨手的就行。但新技術之所以能流行被大家接受,總有它的強大之處,我們自己也不能一直固守偏執於特定的框架和語言,當你還在使用小米加步槍時,別人早已經換成加特林了,想象一下。
多去嘗試新的東西,不要去糾結這個新事物後期會怎麼樣,哪怕你後面用不到,但至少你能從新事物中學習到一些解決問題的新思路,讓自己瞭解更多程式設計的思維。所以,有時候甚至可以跳出工作需要,去學一些和自己目前技術棧差別比較大的技術或語言,以衝擊自己的思維禁錮。