这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
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 的魅力
github.com/JetBrains/c…

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



















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)


![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
