3 小时上手 Decompose:一套业务逻辑通吃 Android/iOS/Web 的组件化实战

发布时间:2026/8/21 12:08:18
3 小时上手 Decompose:一套业务逻辑通吃 Android/iOS/Web 的组件化实战 3 小时上手 Decompose一套业务逻辑通吃 Android/iOS/Web 的组件化实战【免费下载链接】DecomposeKotlin Multiplatform lifecycle-aware business logic components (aka BLoCs) with routing (navigation) and pluggable UI (Jetpack Compose, SwiftUI, JS React, etc.)项目地址: https://gitcode.com/gh_mirrors/de/Decompose同一个列表页Android 写一遍 Java/KotliniOS 写一遍 SwiftWeb 再写一遍 TypeScript三个平台三份 CRUD三份弹窗三份网络层改一处需求要同步改三处。这就是大多数跨平台团队的真实日常。而Decompose给出的答案很干脆把业务逻辑全部抽成不依赖任何 UI 框架的组件一份 Kotlin 代码在三个平台共享UI 只负责画逻辑只负责算。先回答新手最想问的 5 个问题1. Decompose 和 Jetpack Compose 是什么关系毫不相关又天然互补。Decompose 只关心逻辑层——组件、生命周期、状态、导航Compose以及 SwiftUI、React只是它可插拔的皮肤。同一个组件在 Android 上接 Compose在 iOS 上接 SwiftUI组件代码一行不用改。2. 它和 ViewModel 有什么区别ViewModel 是 Android 平台的特产到了 iOS 就没了。Decompose 的组件Component是纯 Kotlin 的普通类配上ComponentContext后自带生命周期、状态保存、返回键处理Android/iOS/Web 通吃概念上更接近跨平台的 ViewModel。3. 组件之间怎么通信三种主流方式父组件把回调 lambda 传给子组件最常用、用Value/MutableValue暴露状态观察者模式、通过导航配置传递参数路由。后文会逐一演示。4. 用了它我还能用 StateFlow 吗能。Decompose 自带的Value是协议中立的观察容器就是为了不绑架你用协程。你在组件内部随便用 StateFlow只要对外暴露成Value即可两端互不干扰。5. 学习成本高吗核心 API 一只手数得过来ComponentContext、childStack、StackNavigation、Value/MutableValue。看懂这四个就已经能上手 80% 的场景了。核心概念工位、走廊和你的部门经理先建立一个直观类比后面所有代码都会复用这套比喻。组件Component一个独立工位只干自己的活比如Todo 列表编辑页对外不暴露实现细节。组件上下文ComponentContext工位之间的走廊也是每个工位的水电管网。它通过委托模式接入组件让组件瞬间拥有生命周期何时创建、何时销毁、状态保存旋转屏幕不丢数据、返回键处理、实例保留类似 AndroidX ViewModel。BLoC 三层结构每个组件内部都按状态State→ 组件门面Component→ UIView组织UI 只订阅组件暴露的状态并转发用户事件中间隔着一道严格的边界。上面这张图就是官方的组件结构图逻辑层左侧只依赖状态和事件UI 层右侧通过门面拿到状态、触发事件两侧通过接口解耦。你要记住的核心结论是UI 依赖组件组件绝不依赖 UI。这句是 Decompose 一切设计的出发点。动手之前先想清楚目录怎么摆很多新手上来就写代码结果组件和 UI 糊成一团。我建议的目录规划KMP 标准结构shared/ ├── commonMain/kotlin/ # 全部业务逻辑各平台共享 │ └── com/example/todo/ │ ├── RootComponent.kt # 根组件导航枢纽 │ ├── list/ # 列表组件 │ ├── editor/ # 编辑组件 │ └── model/ # TodoItem 等纯数据模型 ├── androidMain/ # 仅 Android 平台代码 ├── iosMain/ # 仅 iOS 平台代码 └── jsMain/ # 仅 Web 平台代码设计铁律commonMain 里禁止出现任何android.、UIKit、Composable关键字。一旦出现说明你该把那段代码往下沉的平台目录里挪。分享一条判定方法如果一段代码里同时用了import androidx.*和import platform.UIKit.*那它一定待错了地方——commonMain 的代码应该是干净的、任何平台都无感的。实战一个贯穿全篇的 Todo 应用下面我们用一个Todo 列表 → 详情/编辑的小应用走完三条主线状态怎么管 → 页面怎么切 → 各端 UI 怎么接。示例很小但覆盖了 Decompose 的全部核心玩法。主线一状态怎么管10 分钟先定义一个纯数据模型和组件的状态这些都不依赖任何框架data class TodoItem( val id: String, val title: String, val done: Boolean false, ) data class TodoListState( val items: ListTodoItem emptyList(), val loading: Boolean false, )两个 data class仅此而已没有任何注解和框架依赖。这就是跨平台的第一步——模型层天然可共享。接着写组件。组件是一个普通类通过委托拿到ComponentContextclass TodoListComponent( componentContext: ComponentContext, // 走廊/水电管网 private val onOpenEditor: (TodoItem) - Unit, // 向上层说我要跳转 ) : ComponentContext by componentContext { // 委托组件自身即上下文 private val _state MutableValue(TodoListState()) val state: ValueTodoListState _state // 对外只暴露只读的 Value fun onAddClicked() onOpenEditor(TodoItem(id newId(), title )) fun onToggle(item: TodoItem) { _state.value _state.value.copy( items _state.value.items.map { if (it.id item.id) it.copy(done !it.done) else it } ) } }逐行翻译这段在做什么ComponentContext by componentContext委托语法。让TodoListComponent自己就是一个组件上下文于是组件内部直接能调用lifecycle、instanceKeeper这些能力不用到处传对象。MutableValue/ValueDecompose 自带的可观察状态容器。MutableValue写Value读。对外暴露只读的Value是防止外部代码乱改状态的经典封装手法。onToggle用copy()生成新状态再整体赋值。不要改完原对象就赋值一定要用不可变数据 复制这是状态一致性的前提。为什么不直接用 StateFlow官方文档里明确说了Value让 Decompose 不依赖 Kotlin 协程——你可以继续用 Reaktive、RxJava 或任何响应式库并且Value因为是类而非接口还能直接桥接到 ObjC/Swift 那边做原生监听。想看完整设计思路可以翻 docs/component/overview.md。主线二页面怎么切5 分钟配置导航导航是 Decompose 最能打的功能因为导航发生在逻辑层不在 UI 层。切页就是把配置 A 换成配置 B这种纯数据操作跟平台毫无关系。先定义导航配置用密封类表达当前是哪个页面sealed interface Config { data object List : Config data class Editor(val id: String?) : Config }然后写根组件把导航枢纽搭起来class RootComponent( componentContext: ComponentContext, ) : ComponentContext by componentContext { private val navigation StackNavigationConfig() private val stack childStack( source navigation, initialConfiguration Config.List, // 启动页 handleBackButton true, // 返回键自动出栈 childFactory ::createChild, // 配置 - 组件的工厂 ) override val childStack: ValueChildStackConfig, Child stack private fun createChild(config: Config, componentContext: ComponentContext): Child when (config) { is Config.List - Child.List( TodoListComponent( componentContext componentContext, onOpenEditor { item - navigation.push(Config.Editor(item.id)) // 跳转 }, ) ) is Config.Editor - Child.Editor( TodoEditorComponent( componentContext componentContext, todoId config.id, onBack { navigation.pop() }, // 返回 ) ) } }这段是全局的交通枢纽拆开讲三个重点StackNavigationConfig一个发布导航事件的源提供push、pop、replaceAll等方法。它只操作配置列表不碰任何组件实例。childStack(...)导航的核心函数。它监听导航事件维护一个配置栈并调用childFactory把每个配置变成真实的组件实例。返回的ChildStack里有active当前页和backStack历史栈两个字段UI 层就是靠它决定渲染谁。handleBackButton true一行配置Android 返回键、iOS 侧滑手势、Web 浏览器后退就都能自动触发pop。这行是一个逻辑通吃三端最直观的证据。关于导航接口的底层实现源码在decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/目录ChildStackFactory.kt里能看到childStack的完整签名包括可选的serializer参数——传了它导航栈本身也能序列化保存。扩展阅读docs/navigation/stack/navigation.md。主线三各端 UI 怎么接Android 篇现在 UI 登场。先看 Android Jetpack Compose 的写法Composable fun TodoListUi(component: TodoListComponent) { val state by component.state.subscribeAsState() // 状态桥接到 Compose LazyColumn { items(state.items) { item - Row( Modifier.clickable { component.onToggle(item) } ) { Text(if (item.done) ☑ ${item.title} else ☐ ${item.title}) } } } }subscribeAsState()是extensions-compose模块提供的桥接函数作用就是把 Decompose 的Value变成 Compose 可感知重组的State。UI 层到此为止读状态、调方法没有任何业务逻辑。一个onToggle进去数据更新、UI 刷新全程不需要手动刷新。iOS 端接 SwiftUI 时画风类似只是把subscribeAsState换成 Swift 的StateValue包装器组件类本身原封不动。想体验完整的 Swift 工具StackView、StateValue等可以看sample/app-ios/app-ios/DecomposeHelpers/目录。别忘了入口Android 的根组件初始化最后一步是把根组件挂到 Activity 上。这一步必须在 UI 线程、且在 Compose 启动之前执行class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 1. 创建根组件用系统提供的 defaultComponentContext 帮忙接线 val root DefaultRootComponent(componentContext defaultComponentContext()) // 2. 再启动 Compose setContent { val childStack by root.childStack.subscribeAsState() when (val child childStack.active.instance) { is RootComponent.Child.List - TodoListUi(child.component) is RootComponent.Child.Editor - TodoEditorUi(child.component) } } } }根组件的完整结构对应下面这张层级图——MainActivity只创建RootComponent剩下的导航、状态、子组件全部由逻辑层自己管理注意官方文档反复强调的坑不要直接在 Composable 函数里创建根组件组合过程可能发生在后台线程会破坏组件的线程约定万不得已时要用remember兜底。桌面端同理必须在application {}之前的 UI 线程上创建官方在sample/app-desktop里提供了runOnUiThread工具函数。避坑清单这 4 个坑我替你踩过了坑 1状态只更新不赋值_state.value.update { it.copy(count it.count 1) } // 推荐 _state.value.count 1 // 危险外部可能持有了旧引用记住MutableValue存的是不可变快照任何修改都要通过copy()产出新值再整体赋值。也可以用官方update扩展函数一行搞定。坑 2多线程乱碰状态Value和MutableValue虽然线程安全但官方明确建议订阅和更新都放在主线程。Android 上从 IO 线程拿到数据后记得回到主线程再赋值。坑 3配置重复导致崩溃childStack默认禁止栈内出现重复配置按相等性判断否则直接抛异常。如果你的业务确实需要两个相同页面比如打开两个相同商品的详情页要去DecomposeSettings.duplicateConfigurationsEnabled打开开关别硬碰。坑 4忘了handleBackButton新手最常见的困惑是返回键怎么没反应。检查两点childStack里有没有设handleBackButton trueAndroid 里有没有把OnBackPressedDispatcher传给defaultComponentContext。测试方面Decompose 配套的decompose-test-utils模块提供了TestComponentContext组件测试可以完全脱离设备跑 JVM 单测——这是组件化架构白捡的红利组件本来就该是无 UI 纯逻辑的。效果与展望这套架构到底值不值看一张真实项目官方 sample的组件层级图逻辑层清清楚楚UI 层一碰即换直观收益可以量化逻辑复用率同一个组件类在 AndroidCompose、iOSSwiftUI、桌面、Web 四端共享UI 代码大幅缩水。官方 sample 里共享代码量是各端 UI 的好几倍。测试成本组件是纯 Kotlin 类JVM 单测即可覆盖无需模拟器和真机测试速度从分钟级降到秒级。导航透明化页面切换是纯数据操作深链、浏览器历史webhistory、状态恢复都是同一套机制不用各端各写一遍。生命周期这块 Decompose 也做到了一套模型走天下组件状态在创建、开始、恢复、停止、销毁之间流转和 Android Activity 生命周期语义对齐却在 iOS/Web 上同样成立见下图写在最后Decompose 的哲学一句话就能概括把业务逻辑做厚把 UI 做薄让一次编写、处处运行从口号变成工程现实。想继续深挖你可以从这几个入口开始组件与状态基础看 docs/component/overview.md导航进阶深链、Web 浏览器历史看 docs/navigation/Compose 扩展看 docs/extensions/compose.md。获取完整源码与官方示例git clone https://gitcode.com/gh_mirrors/de/Decompose多平台应用里的组件化难题Decompose 给了你一套值得抄的作业。【免费下载链接】DecomposeKotlin Multiplatform lifecycle-aware business logic components (aka BLoCs) with routing (navigation) and pluggable UI (Jetpack Compose, SwiftUI, JS React, etc.)项目地址: https://gitcode.com/gh_mirrors/de/Decompose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考