作者 |Sahil Sharma譯者 |不想翻身的魚原文 |https://juejin.cn/post/6955015172343726111
2021,APP 的未來已經開始,那就是聲明式UI和跨平台,它們將永遠改變 APP 的架構模式和實現。
未來去創建一個多平台的有85%的共享代碼的原生的UI的 APP 將會變得很正常,開發的生產力將大幅度提高,同時也能提高 APP 的質量。

本文將介紹主要的概念,以及它們是怎麼優雅的組合到一起的。


這裡的過去說的是 APP 一直以來的開發方式:大部分公司都是各自平台開發應用(Android,iOS,Web),在客戶端側沒有做代碼共享。
為了控制在每個平台側編寫重複的代碼,大部分應用傾向於「輕客戶端」,將大部分的業務邏輯和數據處理放在唯一可以共享的服務端。
這種方式,服務端會變得「面向UI」。架構會被設計成這樣:大部分點擊都會觸發一個API的調用,接口會返回下一個頁面要展示的詳細信息,這樣一個非常有限的客戶端邏輯。其他的客戶端邏輯都需要在每個平台維護一套相同的代碼,通常會避免這種情況,除非這些邏輯能帶來比較有意義的客戶體驗。
對於客戶端側的共享代碼,這些年已經有一些公司在嘗試一些方法,但是對於大部分來說都是一次失敗的歷史,最終還是會把代碼還原到只有原生或者平台特性的開發。被大家熟知的案例比如 DropBox(通過共享C++代碼),AirBnB(通過RN共享)。換句話說,還沒有一個合適的技術可以讓這些公司可以實現一個長期安全的投入到共享代碼。
未來2020年,我們經歷了兩個重要範例增長,兩個幾乎是並行的:聲明式UI和Kotlin跨平台。這會帶來前所未有的機會,會讓跨平台和客戶端側的代碼共享變成APP開發的更傾向的選擇。
聲明式UI非常適合跨平台的架構,因為它們是無狀態的而且可以做到跟業務邏輯完全解耦。通過把聲明式UI和Kotlin跨平台組合到一起,我們可以安全的搭建一個有大量客戶端側共享代碼的APP(高達85%),並且在各自原生平台有很高的性能。同時我們還能擁有各自平台原生的UI。
應用現在可以做到「富客戶端」,因為客戶端側的邏輯成本不像以前那麼高了,因為不再需要每個平台維護同一套邏輯了。應用會變得非常的靈活並且帶來很多會給用戶體驗帶來改善,減少用戶在點完一個東西需要等待場景。
服務端可以變得完全是「UI無關的」並且集中精力在提供通用數據,刪除所有的冗餘邏輯,因為數據處理和格式化在客戶端層就可以完成。這個同時也能很大程度提高數據吞吐量。
讓我們一個個來看一下。首先讓我們定義一下這個即將到來的APP開發新紀元的幾個核心概念。
D-KMP 架構未來應用的3個核心概念
我們稱其為D-KMP架構,表示聲明式UI和Kotlin跨平台。MVI模式是為了讓兩者配合起來更完美。

有一點很必須注意的是,現在展示的 D-KMP架構 是針對全新的項目。
我們討論的不是往一個現有項目裡面逐漸的引入聲明式UI和Kotlin 跨平台。我們的目標是簡潔的,健壯的,不會過時的的架構,而不會向過去妥協並且是基於創新的技術和範例來構建的。
還有一點需要強調的是 D-KMP 不是一個 lib 庫,而是一個架構,完全依賴官方的 framework。
讓我們詳細的了解下這個架構三個核心的細節,首先看下聲明式UI。
聲明式UI已經開始在 Android 和 iOS 上發展了差不多經過10多年,我們開始經歷移動框架的非常重要的革命。Android 和 iO S都開始了各自新的UI工具集,並且都是聲明式的,受到 React 和 Flutter 它倆的影響。它們將完全取代現有的各自系統定義視圖的方式。

Google 在 2019 年的 Google I/O 大會上發布了 Jetpack Compose。2020年8月進入 Alpha 階段,2021年春季進入 beta 階段,預計2021年底能發布 1.0 版本。
Jetpack Compose 會支持 Android 5 以及以上的版本(target API 21)。這就意味着所有新的 Jetpack Compose 的 API 都是向後兼容的,而且不需要新的 Android 版本。這是因為在低版本上 Jetpack Compose 是直接在畫布上進行繪製。
Apple 在2019年的 WWDC 上個發布了 Swift UI,隨着iOS 13一起發布。今年隨着iOS 14的發布已經做了很多改進。
跟 Jetpack Compose 不同的是,Swift UI 的更新是跟 iOS 系統綁定在一起的。新的 Swift UI API 不會向後兼容。但是考慮到所有支持 iOS 13的設備現在也支持 iOS 14(與往常不同,蘋果今年沒有棄用任何設備),可以很安全的在 target 是 iOS14 的應用使用 Swift UI。
為什麼是聲明式UIJetpack Compose 和 Swift UI 都是聲明式UI的框架,它們只是用來表示不同狀態下UI應該怎麼樣展示,而不是直接的管理狀態。聲明式UI變得越來越流行,也正是因為 React.js 和 Flutter 這來你給個框架,它倆讓人們看到了跟無狀態的組件交互是一件多麼簡單的事情。也正是它倆的成功,讓 Android 和 iOS 加入到了聲明式UI的世界。
使用 Jetpack Compose 你可以忘記 Android 笨重的視圖系統和可怕的 Fragments。使用 Swift UI 你可以忘記 VC 這個 UI Kit 和不怎麼靈活的 StoryBoard 。這是一個全新的開始。這就是未來!


聲明式UI可以讓UI布局和ViewModel進行一個徹底的分離,因為不再需要其他層額外的代碼(使用findViewById和@IBOutlet)去把兩者聯繫起來。
Jetpack Compose和Swift UI非常的相似。有一些瑣碎的語法的(Jetpack Compose用的是Kotlin,Swift UI用的是Swift)以及導航模式不同,但是背後帶理念都是一樣的。尤其是數據是傳遞到這些無狀態UI框架的方式完全是一樣的。也正是因為這一點,ViewModel跟平台無關才顯得有意義。後面我們會進一步聊下這方面具體的細節。
Web端的聲明式UIWeb端最有名的聲明式UI就是React.js(by Facebook),也是真正把聲明式UI帶向成功的框架。這是一個工業級改變的框架,如果沒有React.js的成功,我們現在也可能在Android和iOS上用不了聲明式UI。
Kotlin提供了對React.js非常方便的封裝,我們可以使用Kotlin/React作為Web的聲明式UI。它可以以插件的形式引入到我們的D-KMP架構,就跟Android上的Jetpack Compose和iOS上的Swift UI一樣。
在Kotlin/React裡面你可以引用所有已有的React.js的組件,可以用到Kotlin語言的相對於JS的優點。你也可以完全用Kotlin創建自定義的組件。可以到 Kotlin/React 文檔了解詳細的信息。
除了Kotlin/React以外,更有意思的可能就是Compose for Web,也就是Jetpack Compose的Web版本,目前JetBrains(Kotlin的創建者)正在開發。如果說用Kotlin/React 來實現UI還需要額外15%的工作量(相對於85%的KMP的共享代碼),Compose for Web會使這塊工作量少很多,因為它跟Android的Jetpack Compose非常像。因此我們非常期待它發布的那一天。
桌面端的聲明式UI在我們等待 web版本的同時,JetBrains 已經發布了桌面版的 Compose,可以用來開發 Windows,macOS 和 Linux 的桌面應用。
關於桌面端,值得注意的是 Swift UI 已經支持 macOS 了。用 Swift UI 編寫的UI在 iOS,macOS,tvOS 和 watchOS 6 之間是無縫適配的。我們也期望 Jetpack Compose 早日實現多平台的無縫適配(Android,桌面和Web)。
可以想象一下,在不久的將來只要你會 Jetpack Compose 和 Swift UI 就能開發出各個平台非常優秀的應用。

你可能在想會不會只有這麼一套聲明式UI框架可以適配所有的平台。
換句話說,Jetpack Compose 會不會最終適配所有蘋果的系統(iOS,macOS,tvOS,watchOS),或者說 Swift UI 最終會不會適配非蘋果的設備?
就目前而言,如果這個會發生的話,那也不會是蘋果或者谷歌來做,可能是社區或者第三方來做的。我們已經看到 Jetpack Compose 已經被引入到桌面端和Web端,不是谷歌而是 JetBrains。
谷歌和蘋果還是會集中精力到各自的系統上面。其實這樣對UI的未來是非常健康的。這樣就一直有兩套獨立強大的工具集相互競爭,不斷創新。
2. Kotlin 跨平台和 MVI模式
隨着2020年8月份Kotlin 1.4的發布,Kotlin跨平台已經不再是實驗性階段,已經是 Alpha 了。
現在,其實我們已經可以開始應用這項技術到我們的產品上了,雖然後面還會有一些改變和改進,但是目前的穩定性已經很好了。
如果目前還有什麼會阻塞我們實現這個 D-KMP項目的話,它們都來自UI部分(Jetpack Compose還處在密切開發中,但是隨着 Navigation 組件的發布已經越來越好了)而不是KMP的部分。
由於 Kotlin 在 JVM 社區的成功,2017年穀歌宣布將 Kotlin 作為 Android 第一支持的語言。2019年穀歌將 Kotlin 指定為 Android 開發的首選語言(取代了Java)。
現在 Kotlin 已經演進為一個跨平台的語言,擁有將代碼編譯到3個不同的平台:

正是因為這個,我們現在可以用Kotlin開發共享代碼直接運行到各個平台上。
目前有兩個不同首字母用到跨平台上面:
自從Kotlin 1.4開始,已經有一個固定的KMM的門戶入口,專門用來說明怎麼在移動端開發開始跨平台。對於不熟悉 Kotlin 跨平台的人來說是非常好的材料。
Kotlin 並不是唯一實現跨平台的編程語言。同時也是一個非常有趣的編程語言,同時避免了很多的模板代碼。它有很多你能想象的高級特性:協程,可計算屬性,屬性委託,擴展函數,高階函數,lambda等等。
Kotlin 很快的成為了一門主流的編程語言。用 Kotlin 寫的代碼肯定會持續數十年的。作為一個長期項目來用肯定是沒問題的。
KMP vs Flutter vs ReactNative當說起跨平台,2個主流的框架大家聽的最多的可能是 Flutter 和 React Native ,這兩個框架都可以讓你進行共享代碼,
同時你也可能聽到大家不喜歡這些框架,因為它們限制了大家定製各個平台的原生UI。
Kotlin跨平台的到來,同時提供了這兩個優點:

在 KMP 裡面,共享的代碼是用 Kotlin 寫的,但是最終會編譯成一個原生的庫:Android 上是一個 jar包,iOS 上是一個 OC 的 framework,web 上是一個 js 的庫。因為這樣,原生的UI層在各自的平台上可以很自然和共享代碼進行交互。
在 Flutter 裡面,代碼是用 Dart 來寫的,最終會編譯成一個原生的庫,在 Android 上是通過 NDK,iOS上是 LLVM,Web 上是 JS。但是與 KMP 不同的是,Flutter 需要開啟一個自己的引擎,會對包體積的影響比較大。Flutter 不使用原生的UI,它使用的是自己的聲明式UI widget是通過 Skia 的圖像引擎一個像素一個像素畫出來的。這兩年有很多人在用 Flutter,因為 Flutter 提供的聲明式UI方式而不是傳統的 Android 和 iOS 的 View。但是現在聲明式的UI已經在 Android 和 iOS 的原生端開始加速支持了,這樣 Flutter 的主要優勢已經不在了。Jetpack Compose 和 SwiftUI 可以讓你全速的構建一個頂級的APP。
在React Native,代碼是用javascript寫的,只能通過運行JS代碼的C/C++橋跟native 層進行通信。UI的組件是對Android和iOS原生組件的封裝,開發者對於UI的控制非常有限。RN整個的架構也證明性能不是特別好,甚至Facebook自己也慢慢的要放棄RN了。2018年AirBnB就宣布RN的時代結束了。
語言很重要:Kotlin vs Dart vs JavaScript另外一個 KMP 相對於 Flutter 和 RN 的優勢就是編程語言。與 Dart 和 JS 相比,Kotlin 是下一代頂級語言,它具有可靠性和簡潔的特點。在 Kotlin 很輕鬆的就能寫出高質量的代碼,因為它具備了非常智能的特性,比如協程,可計算屬性,高階函數等等。
D-KMP架構下平台特有的代碼只有15%此時可能有人會認為 「好,我知道KMP很牛。我知道聲明式UI最終會來到 Android 和 iOS 平台。但是這種方式我仍然還要給各自平台畫UI,這也會造成很多重複工作!」
答案是「NO!並沒有很多重複的工作」
在新的原生聲明式UI平台下,UI層是非常輕的。在我們目前用 D-KMP 架構構建的 APP 里,從整個代碼量來看UI層大約只占了15%的代碼量。並且 UI 是所有的平台特有的代碼,其他的全是 KMP 的共享代碼。

這額外的15%的代碼是完全值得的,因為它可以讓我們給各個平台自由定製,而不是像 Flutter 和 RN 有很多的限制。Android 和 iOS 是兩個不同的平台有很多的不同。一個頂級的 APP 需要保持各個平台真實的 UI/UX 模式的。
從我們的經驗來看,一旦我們在 Android 端寫了 Jetpack Compose 的 UI,就可以直接到 iOS 上 SwiftUI 實現出來。代碼的結構幾乎是一樣的。對於一個簡單的 APP,要不了一天時間。
在兩個框架還有一些相同的組件,雖然名字不同。例如,你想要把多個文本水平放置在一個布局內,Android 端 Jetpack Compose 是 Row,iOS 端 SwiftUI 是 HStack,文本的組件在兩個框架裡面都是 Text,只是語法有點不一樣。一旦你熟悉了這些小小的不同點,完成一端的聲明式UI後,你可以快速複製到另外一個上面。

所有的數據都來自於頁面的狀態,由 KMP 共享的代碼提供。在各自平台的聲明式UI,只需要關心 view,其實是一個很輕的工作。
重要的是聲明式UI不需要處理數據。啥也不用管直接展示就好。這也會大量減少平台相關的bug。
一旦共享的代碼不擔心bug,在各自平台上所有的事情就會推進的很順利。這也是為什麼不需要過於在意在各自平台上開發一套UI。
MVI模式:D-KMP架構的第三個核心我們一開始就提到,MVI模式(代表Model-View-Intent) 是這個結構的第三個核心部分。背後主要的概念是單項數據流,這也是它跟之前的 MVC,MVP,MVVM 不同的地方。MVI 是響應式模式,讓 APP 的行為更加的一致性和可預測性,你可以認為它是 MVVM 的演進。

在 MVI 模式下,View的狀態只有唯一的可信數據源。任何時候狀態都是有可變的數據組成,並且只能被 Model 修改。所有的事情都是單向的。用戶觸發了一個事件/intent。Model做出相應並執行一些操作之後改變狀態。然後新的狀態被反映到 View 上面。
在我們的 D-KMP 架構下,我們在 KMP 的共享代碼裡面實現 MVI 的 Model(可見下面的圖)這個可以讓我們在共享代碼裡面進行狀態管理,這個非常重要!
也正是因為這樣,我們平台特有的代碼只是聲明式 UI 的那一層,這一層是很輕的並且是無狀態的,因為它完全把狀態的管理委託給了 KMP 的 ViewModel。


在我們的 D-KMP 架構下,ViewModel 是一個 Kotlin 跨平台的類,它有5個組件:
classDKMPViewModel(repo:Repository){valstateFlow:StateFlow<AppState>get()=stateManager.mutableStateFlowprivatevalstateManagerbylazy{StateManager(repo)}valnavigationbylazy{Navigation(stateManager)}valstateProviderbylazy{StateProvider(stateManager)}valeventsbylazy{Events(stateManager)}}簡單的描述一下這幾個組件。
StateFlowViewModel 裡面的 StateFlow 是負責觸發UI層進行重組的組件,每當它的值發生改變就會觸發UI重組。
從上面的定義你可以發現,它的類型是一個叫 AppState 的數據類,定義的非常簡單只持有了一個 recompositionIndex 的屬性:
dataclassAppState(valrecompositionIndex:Int=0)StateFlow 是一個只讀組件,從讀/寫版本中讀取自己的值(通過一個getter的屬性計算),MutableStateFlow 被定義為 StateManager 組件的一個屬性。
StateManagerStateManager 是我們 ViewModel 的核心類。它管理者屏幕的狀態和協程的scope。它還持有了 MutableStateFlow 這個負責修改 AppState 這個 StateFlow 的值然後觸發UI的重組。
classStateManager(repo:Repository){internalvalmutableStateFlow=MutableStateFlow(AppState())valscreenStatesMap:MutableMap<ScreenIdentifier,ScreenState>=mutableMapOf()valscreenScopesMap:MutableMap<ScreenIdentifier,CoroutineScope>=mutableMapOf()internalvaldataRepositorybylazy{repo}funtriggerRecomposition(){mutableStateFlow.value=AppState(mutableStateFlow.value.recompositionIndex+1)}}NavigationNavigation 被所有平台共享,很好的保證了多平台的一致性。也正是因為這個,不同平台的頁面可以用幾乎相同的語法來定義。下面貼上了我們實例工程 maste/detail 裡面的的頁面定義(github倉庫的鏈接在本文最後)。
Compose上面(Kotlin):

SwiftUI上面(Swift):

StateProvider 給頁面提供狀態。UI重組的時候會調用,並且從 StateManager 的 screenStatesMap 裡面獲取數據。
...Screen.CountriesList->CountriesListScreen(countriesListState=stateProviders.get(screenIdentifier)...)...EventsEvents 是定義在共享代碼裡面的函數,可以被各自平台的UI層調用。它們通常是執行一些能蓋面APP狀態的操作,然後觸發新UI的重組。
...Screen.CountriesList->CountriesListScreen(...onFavoriteIconClick={events.selectFavorite(countryName=it)})...在文章的最後我貼了 github 倉庫鏈接,可以把代碼拉下來自己跑一下可能更容易理解這些組件。


關於平台特性的代碼,首先我們需要通過平台特性的工廠方法創建一個 DKMPViewModel 的實例。
在 Android 側,需要把 applicationContext 作為參數傳進去。因為在 Android 的框架下很多東西沒有應用上下文無法工作,比如 sqlite 數據庫。
DKMPViewModel.Factory.getAndroidInstance(context)在iOS側,我們不需要任何參數。
DKMPViewModel.Factory.getIosInstance()收集數據流(StateFlow)我們架構裡面非常重要的一個一部分就是 StateFlow(跟平台無關的觀察者),它可以通過監聽 AppState 狀態的變化,來觸發UI層的重組(recomposition)。
在我們的框架裡面你需要記住一點,AppState 就是一個 recompositionIndex 的值,每次 StateManager 調用 triggerRecomposition() 會讓它的值加+1。
每次進行重組,UI 都能通過 StateProvider 拿到一個新的狀態。
接下來我們看一下如何在各個平台上配置 StateFlow。
Android側在 Android 上使用 StateFlow 非常的直接,一行代碼就能搞定。是因為 Android 團隊已經實現了 StateFlow 裡面的 collectAsState() 方法作為 Jetpack Compose 這個庫的一部分。
lassDKMPApp:Application(){lateinitvarmodel:DKMPViewModeloverridefunonCreate(){super.onCreate()model=DKMPViewModel.Factory.getAndroidInstance(this)}}classMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)valmodel=(applicationasDKMPApp).modelsetContent{MyTheme{MainComposable(model)}}}}@ComposablefunMainComposable(model:DKMPViewModel){valappStatebymodel.stateFlow.collectAsState()valdkmpNav=appState.getNavigation(model)dkmpNav.Router()}iOS側目前在 iOS 上收集數據流還需要寫一些模板代碼,但是我們希望 JetBrains 能儘快優化一下。同時我們需要初始化一個「listener」在平台特性的ViewModel上:
classAppObservableObject:ObservableObject{letmodel:DKMPViewModel=DKMPViewModel.Factory().getIosInstance()vardkmpNav:Navigation{returnself.appState.getNavigation(model:self.model)}@PublishedvarappState:AppState=AppState()init(){model.onChange{newStateinself.appState=newState}}}@mainstructiosApp:App{@StateObjectvarappObj=AppObservableObject()varbody:someScene{WindowGroup{MainView(appObj:appObj)}}}structMainView:View{@ObservedObjectvarappObj:AppObservableObjectvarbody:someView{letdkmpNav=appObj.dkmpNavdkmpNav.router()}}onChange 是作為一個擴展函數加到 ViewModel 上面,只有 iOS 需要。
funDKMPViewModel.onChange(provideNewState:((AppState)->Unit)):Closeable{valjob=Job()stateFlow.onEach{provideNewState(it)}.launchIn(CoroutineScope(Dispatchers.Main+job))returnobject:Closeable{overridefunclose(){job.cancel()}}}D-KMP數據層在我們的 D-KMP 架構下,我們希望 ViewModel 和數據層是完全分離的。

數據層由 Repository 組成,Repository 從各種數據源獲取數據,比如 webservice,運行時對象,平台配置,平台服務,sqlite 數據庫,實時數據庫,本地文件等等。
Repository 也負責管理本地的緩存機制,這樣也能保證更好的做到只寫一次,在各個平台上都能運行。不需要再各自平台再實現一遍。
Repository 的角色是負責處理數據然後將未經格式化的數據給 ViewModel,在 ViewModel 裡面對數據進行格式化。
ViewModel 不需要關心數據源,數據源或者緩存都是由 Repository 來負責管理。
KMP庫在 KMP 裡面,我們當然不能用平台特有的庫。但是也不用擔心,因為新的 KMP 庫會比舊的庫做的更好。有的完全是用 Kotlin 重寫了,有的可能是底層對原生庫的封裝。不管怎麼樣,你可以不用關心它具體是怎麼實現的。
下面這個面,總結一些重要庫目前的KMP支持情況:

對於聲明式UI,StateFlow 和 Coroutines。我們快速地過一下其他幾個吧:
除了已經存在的 KMP庫,任何人都可以自己開發一個KMP的庫。用 expect/actual 的特性來實現,這樣就能將各自平台特性給封裝起來。
有一些KMP庫可能馬上會發布:
現在讓我們看一下 D-KMP架構下怎麼來組織開發團隊,其實跟傳統的APP開發還是有比較大的不同的。我們有4個主要的角色:

我們堅信在 D-KMP 的團隊裡面,UI開發應該是跨平台的,負責實現 Jetpack Compose 和 SwiftUI 的實現。考慮從到聲明式UI框架的簡單特點,同一個開發者來負責兩端完全是可行的(也是有趣的)。聚焦到兩個框架上,可以讓開發者能更好的理解UI的發展趨勢,以及實現更好的用戶體驗。
ViewModel 開發這個角色非常重要,某種程度上需要承擔一些管理的任務。是整個開發過程中的中心位置,需要對整個工程很了解。ViewModel 的開發者需要同UI開發者一起定義所有頁面狀態的對象,還要和數據層開發者一起定義需要的數據。同時 ViewModel 開發者還需要組織國際化的問題。
數據層(DataLayer)開發這是一個對技術要求非常高的角色。數據層開發需要處理所有跟數據有關的東西,包括數據的緩存機制等等。數據開發需要組織好所有的數據,有時候甚至包括平台特性的數據源,比如定位或者藍牙服務。這個角色需要對 Kotlin 跨平台非常熟悉,有時候設置需要寫一些自定義的跨平台庫。
後端(Backend)開發在 D-KMP 的團隊裡面,這個角色仍然重要,因為需要和所有的app團隊進行合作(Android,iOS,Web),但是又沒有平台特性開發那麼重要。在 D-KMP 架構下,後端開關直接和數據層開發打交道。數據層開發把 APP 需要哪些數據定好。後端開發不需要了解具體 APP層發生了什麼。Webservice 可以用任意的語言開發,比如 Golang。如果你對Kotlin全棧感興趣的話,也可以用 Ktor 這個框架,用的 Kotlin/JVM 技術。在服務端用 Kotlin 來寫還有另外一個好處,可以讓 APP 的數據層和服務端共用數據類的定義。
KMP示例工程
-- End --
推薦閱讀
Compose 架構如何選?MVP & MVVM & MVI
Compose Multiplatform 正式官宣,與 Flutter 必有一戰?
MVI 架構:從雙向綁定到單向數據流
加好友進交流群,技術乾貨聊不停

