不止 Android!Compose Multiplatform 来了

简介: 7月底 Compose for Android 1.0 刚刚发布,紧接着 8月4日 JetBrains 就宣布了 Compose Multiplatform

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

image.png

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端代码

项目基于 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 : 声明式 UI 的基础 一文中,我曾介绍过 Compose 跨平台的技术基础,如今配合各种 KM 三方库,使得开发生态更加完整。 Compose Multiplatform 全程基于 Kotlin 打造,上下游同构,相对于 Flutter 和 RN 更具优势,未来可期。

目录
相关文章
|
前端开发 算法 PHP
Jetpack Compose Runtime : 声明式 UI 的基础
Jetpack Compose Runtime : 声明式 UI 的基础
332 0
|
算法
【MATLAB】 SSA奇异谱分析信号分解算法
【MATLAB】 SSA奇异谱分析信号分解算法
847 0
|
C语言
经典面试题:嵌入式系统中经常要用到无限循环,怎么样用C编写死循环呢
在嵌入式系统开发中,无限循环常用于持续运行特定任务或监听事件。使用C语言实现死循环很简单,可以通过`while(1)`或`for(;;)`的结构来编写。例如:`while (1) { /* 循环体代码 */ }`,这种写法明确简洁,适用于需要持续执行的任务或等待中断的场景。
|
XML 前端开发 小程序
用Prompt技巧激发无限创意
本文深入探讨当前最前沿的prompt engineering方案,结合OpenAI、Anthropic和Google等大模型公司的资料,以及开源社区中宝贵的prompt技巧分享,全面解析这一领域的实践策略。
649 13
|
数据采集 运维 算法
大数据项目管理:从需求分析到成果交付的全流程指南
【4月更文挑战第9天】本文介绍了大数据项目从需求分析到成果交付的全过程,包括需求收集与梳理、可行性分析、项目规划、数据准备与处理、系统开发与集成,以及成果交付与运维。文中通过实例展示了如何进行数据源接入、数据仓库建设、系统设计、算法开发,同时强调了需求理解、知识转移、系统运维的重要性。此外,还提供了Python和SQL代码片段,以说明具体技术实现。在大数据项目管理中,需结合业务和技术,灵活运用这些方法,确保项目的成功执行和价值实现。
3492 1
|
Ubuntu 网络安全 开发者
CTF本地靶场搭建——GZ:CTF安装
GZCTF是一个开源的网络安全竞技平台,由GZTimeWalker维护,提供环境供爱好者和专业人士通过解决CTF题目提升技能。本文档介绍了在Ubuntu 20.04上部署GZCTF的步骤,包括安装操作系统、配置清华镜像源、更新软件、安装Docker和docker-compose,以及根据GZCTF文档创建配置文件并启动服务。完成部署后,用户可在浏览器中看到GZCTF平台。
|
供应链 安全 数据安全/隐私保护
来自海光、南湖实验室等 14 位安全专家,共同探讨开源软件安全生态等焦点问题
来自海光、南湖实验室等 14 位安全专家,共同探讨开源软件安全生态等焦点问题。
|
安全 API 开发工具
一文看懂 Jetpack Compose 快照系统
一文看懂 Jetpack Compose 快照系统
344 0