
7月底 Compose for Android 1.0 剛剛發布,緊接著 8月4日 JetBrains 就宣布了 Compose Multiplatform 的最新進展,目前已進入 alpha 階段,
Compose 作為一個宣告式UI框架,除了渲染部分需借助平臺能力以外,其他大部分特性可以做到平臺無關,尤其是 Kotlin 這樣一門跨平臺語言,早就為日后的 UI 跨平臺奠定了基礎,
Compose Multiplatform 將整合現有的三個 Compose 專案:Android、Desktop、Web,未來可以像 Kotlin Multiplatform Project 一樣,在一個工程下開發跨端應用,統一的宣告式范式讓代碼在最大程度上實作復用,真正做到write once,run anywhere ,如今進入 alpah 階段標志著其 API 也日漸成熟,相信不久的未來正式版就會與大家見面,
我們通過官方 todoapp 的例子,提前體驗一下 Compose Multiplatform 的魅力
https://github.com/JetBrains/compose-jb/tree/master/examples/todoapp

todoapp 工程
- todoapp
- common:平臺無關代碼
- compose-ui :UI層可復用代碼(兼容 Android 與 Desktop)
- main:邏輯層可復用代碼(首頁)
- edit:邏輯層可復用代碼(編輯)
- root:邏輯層入口、導航管理(
main與eidt間頁面跳轉) - utils:工具類
- database:資料庫
- android:平臺相關代碼,
Activity等 - desktop:平臺相關代碼,
application等 - web:平臺相關,
index.html等 - ios:compose-ui 尚不支持 ios,但通過KMM配合SwiftUI可以實作iOS端代碼
- common:平臺無關代碼
專案基于 Model-View-Intent(aka MVI) 打造,Model層、ViewModel層 代碼幾乎可以 100% 復用,View層在 desktop 和 Android 也可實作大部分復用,web 有一定特殊性需要單獨適配,

除了 Jetpack Compose 以外,專案中使用了多個基于 KM 的三方框架,保證了上層的開發范式在多平臺上的一致體驗:
| KM三方庫 | 說明 |
|---|---|
| Decompose | 資料通信(BLoC) |
| MVIKotlin | 跨平臺MVI |
| Rektive | 異步回應式庫 |
| SQLDelight | 資料庫 |
todoapp 代碼
平臺入口代碼
對比一下 Android端 與 Desktop端 的入口代碼
//todoapp/android/src/main/java/example/todo/android/MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val root = todoRoot(defaultComponentContext())
setContent {
ComposeAppTheme {
Surface(color = MaterialTheme.colors.background) {
TodoRootContent(root)
}
}
}
}
private fun todoRoot(componentContext: ComponentContext): TodoRoot =
TodoRootComponent(
componentContext = componentContext,
storeFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory())),
database = DefaultTodoSharedDatabase(TodoDatabaseDriver(context = this))
)
}
//todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt
fun main() {
overrideSchedulers(main = Dispatchers.Main::asScheduler)
val lifecycle = LifecycleRegistry()
val root = todoRoot(DefaultComponentContext(lifecycle = lifecycle))
application {
val windowState = rememberWindowState()
LifecycleController(lifecycle, windowState)
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "Todo"
) {
Surface(modifier = Modifier.fillMaxSize()) {
MaterialTheme {
DesktopTheme {
TodoRootContent(root)
}
}
}
}
}
}
private fun todoRoot(componentContext: ComponentContext): TodoRoot =
TodoRootComponent(
componentContext = componentContext,
storeFactory = DefaultStoreFactory(),
database = DefaultTodoSharedDatabase(TodoDatabaseDriver())
)
- TodoRootContent:根Composable,View層入口
- TodoRootComponent:根狀態管理器,ViewModel層入口
- DefaultStoreFactory:創建 Store,管理狀態
- DefaultTodoShareDatabase:M層,資料管理
TodoRootContent 和 TodoRootComponent 分別是 View 層和 ViewModel 層的入口,TodoRootComponent 管理著全域狀態,即頁面導航狀態,
可以看到,Android 與 Desktop 在 View 、 VM 、M等各層都進行了大面積復用,
VM層代碼
MVI 中雖然沒有 ViewModel,但是有等價概念,從習慣出發我們暫且稱之為 VM 層, VM層其實就是狀態的管理場所,我們以首頁的 mian 為例
//todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainComponent.kt
class TodoMainComponent(
componentContext: ComponentContext,
storeFactory: StoreFactory,
database: TodoSharedDatabase,
private val output: Consumer<Output>
) : TodoMain, ComponentContext by componentContext {
private val store =
instanceKeeper.getStore {
TodoMainStoreProvider(
storeFactory = storeFactory,
database = TodoMainStoreDatabase(database = database)
).provide()
}
override val models: Value<Model> = store.asValue().map(stateToModel)
override fun onItemClicked(id: Long) {
output(Output.Selected(id = id))
}
override fun onItemDoneChanged(id: Long, isDone: Boolean) {
store.accept(Intent.SetItemDone(id = id, isDone = isDone))
}
override fun onItemDeleteClicked(id: Long) {
store.accept(Intent.DeleteItem(id = id))
}
override fun onInputTextChanged(text: String) {
store.accept(Intent.SetText(text = text))
}
override fun onAddItemClicked() {
store.accept(Intent.AddItem)
}
}
了解 MVI 的朋友對上面的代碼應該非常熟悉,store 管理狀態并通過 models 對UI暴露,所有資料流單向流動, Value<Model> 是 Decompose 庫中的型別,可以理解為跨平臺的 LiveData
View層代碼
@Composable
fun TodoRootContent(component: TodoRoot) {
Children(routerState = component.routerState, animation = crossfadeScale()) {
when (val child = it.instance) {
is Child.Main -> TodoMainContent(child.component)
is Child.Edit -> TodoEditContent(child.component)
}
}
}
TodoRootContent內部很簡單,就是根據導航切換不同的頁面,
具體看一下TodoMainContent
@Composable
fun TodoMainContent(component: TodoMain) {
val model by component.models.subscribeAsState()
Column {
TopAppBar(title = { Text(text = "Todo List") })
Box(Modifier.weight(1F)) {
TodoList(
items = model.items,
onItemClicked = component::onItemClicked,
onDoneChanged = component::onItemDoneChanged,
onDeleteItemClicked = component::onItemDeleteClicked
)
}
TodoInput(
text = model.text,
onAddClicked = component::onAddItemClicked,
onTextChanged = component::onInputTextChanged
)
}
}
subscribeAsState() 在 Composable 中訂閱了 Models 的狀態,從而驅動 UI 重繪,Column 、Box 等 Composalbe 在 Descktop 和 Android 端會分別進行平臺渲染,
web端代碼
最后看一下web端實作,
Compose For Web 的 Composalbe 大多基于 DOM 設計,無法像 Android 和 Desktop 的 Composable 那樣復用,但是 VM 和 M 層仍然可以大量復用:
//todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt
fun main() {
val rootElement = document.getElementById("root") as HTMLElement
val lifecycle = LifecycleRegistry()
val root =
TodoRootComponent(
componentContext = DefaultComponentContext(lifecycle = lifecycle),
storeFactory = DefaultStoreFactory(),
database = DefaultTodoSharedDatabase(todoDatabaseDriver())
)
lifecycle.resume()
renderComposable(root = rootElement) {
Style(Styles)
TodoRootUi(root)
}
}
將 TodoRootComponent 傳給 UI, 協助進行導航管理
@Composable
fun TodoRootUi(component: TodoRoot) {
Card(
attrs = {
style {
position(Position.Absolute)
height(700.px)
property("max-width", 640.px)
top(0.px)
bottom(0.px)
left(0.px)
right(0.px)
property("margin", auto)
}
}
) {
val routerState by component.routerState.subscribeAsState()
Crossfade(
target = routerState.activeChild.instance,
attrs = {
style {
width(100.percent)
height(100.percent)
position(Position.Relative)
left(0.px)
top(0.px)
}
}
) { child ->
when (child) {
is TodoRoot.Child.Main -> TodoMainUi(child.component)
is TodoRoot.Child.Edit -> TodoEditUi(child.component)
}
}
}
}
TodoMainUi 的實作如下:
@Composable
fun TodoMainUi(component: TodoMain) {
val model by component.models.subscribeAsState()
Div(
attrs = {
style {
width(100.percent)
height(100.percent)
display(DisplayStyle.Flex)
flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
}
}
) {
Div(
attrs = {
style {
width(100.percent)
property("flex", "0 1 auto")
}
}
) {
NavBar(title = "Todo List")
}
Ul(
attrs = {
style {
width(100.percent)
margin(0.px)
property("flex", "1 1 auto")
property("overflow-y", "scroll")
}
}
) {
model.items.forEach { item ->
Item(
item = item,
onClicked = component::onItemClicked,
onDoneChanged = component::onItemDoneChanged,
onDeleteClicked = component::onItemDeleteClicked
)
}
}
Div(
attrs = {
style {
width(100.percent)
property("flex", "0 1 auto")
}
}
) {
TodoInput(
text = model.text,
onTextChanged = component::onInputTextChanged,
onAddClicked = component::onAddItemClicked
)
}
}
}
最后
在 Jetpack Compose Runtime 與 NodeTree 管理
一文中,我曾介紹過 Compose 跨平臺的技識訓礎,如今配合各種 KM 三方庫,使得開發生態更加完整, Compose Multiplatform 全程基于 Kotlin 打造,上下游同構,相對于 Flutter 和 RN 更具優勢,未來可期,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/293215.html
標籤:其他
