Compose Multiplatform 正式官宣,与 Flutter 迟早必有一战?

Posted fundroid_方卓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Compose Multiplatform 正式官宣,与 Flutter 迟早必有一战?相关的知识,希望对你有一定的参考价值。

7月底 Compose for android 1.0 刚刚发布,紧接着 8月4日 JetBrains 就宣布了 Compose Multiplatform 的最新进展,目前已进入 alpha 阶段。

Compose 作为一个声明式UI框架,除了渲染部分需借助平台能力以外,其他大部分特性可以做到平台无关。尤其是 Kotlin 这样一门跨平台语言,早就为日后的 UI 跨平台奠定了基础。

Compose Multiplatform 将整合现有的三个 Compose 项目:AndroidDesktopWeb,未来可以像 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:逻辑层入口、导航管理(maineidt 间页面跳转)
      • utils:工具类
      • database:数据库
    • android:平台相关代码,Activity
    • desktop:平台相关代码,application
    • web:平台相关,index.html
    • ios:compose-ui 尚不支持 ios,但通过KMM配合SwiftUI可以实现iOS端代码

项目基于 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层,数据管理

TodoRootContentTodoRootComponent 分别是 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 刷新。ColumnBox 等 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 更具优势,未来可期。

以上是关于Compose Multiplatform 正式官宣,与 Flutter 迟早必有一战?的主要内容,如果未能解决你的问题,请参考以下文章

Compose Multiplatform 正式官宣,与 Flutter 迟早必有一战?

Compose Multiplatform 正式版将于年内发布

取代传统 Java UI 框架,JetBrains Kotlin 声明式 UI 框架 Compose Multiplatform 1.0 正式上线

10个问题带你看懂 Compose Multiplatform 1.0

10个问题带你看懂 Compose Multiplatform 1.0

Compose Multiplatform 实战:联机五子棋