不止 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 更具优势,未来可期。

目录
相关文章
|
存储 编解码 缓存
H.264编码
H.264编码
412 0
|
缓存 算法
07、Netty学习笔记—(聊天业务优化:参数调优)(二)
07、Netty学习笔记—(聊天业务优化:参数调优)(二)
07、Netty学习笔记—(聊天业务优化:参数调优)(二)
|
监控 网络协议 Java
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)(上)
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)
644 0
|
C语言
经典面试题:嵌入式系统中经常要用到无限循环,怎么样用C编写死循环呢
在嵌入式系统开发中,无限循环常用于持续运行特定任务或监听事件。使用C语言实现死循环很简单,可以通过`while(1)`或`for(;;)`的结构来编写。例如:`while (1) { /* 循环体代码 */ }`,这种写法明确简洁,适用于需要持续执行的任务或等待中断的场景。
|
Web App开发 缓存 网络协议
浏览器原理 20 # Chrome开发者工具:利用网络面板做性能分析
浏览器原理 20 # Chrome开发者工具:利用网络面板做性能分析
833 0
浏览器原理 20 # Chrome开发者工具:利用网络面板做性能分析
|
监控 小程序 安全
小程序全栈开发中的WebSocket实时通信实践是一种高效的开发模式。
随着移动互联网的发展,小程序成为企业拓展业务的新渠道。WebSocket作为一种实时通信协议,可在小程序中实现如实时聊天、推送、游戏等功能。它支持客户端与服务器间的全双工长连接通信,优于传统HTTP。开发者需注意安全、性能及兼容性等问题,以优化体验并保障稳定运行。掌握WebSocket有助于提升小程序功能性与用户体验。
315 1
|
JSON Dart Android开发
Flutter系列:Flutter常见问答(可用于面试)
Flutter系列:Flutter常见问答(可用于面试)
1311 0
|
机器学习/深度学习 编解码 文字识别
Hybrid Multiple Attention Network for Semantic Segmentation in Aerial Images(二)
Hybrid Multiple Attention Network for Semantic Segmentation in Aerial Images
462 0
Hybrid Multiple Attention Network for Semantic Segmentation in Aerial Images(二)

热门文章

最新文章