一、前言
已有项目 View 难以全部升级到 Compose 框架,所以考虑在传统 Android View 视图框架中接入 Compose 框架,也就是 View 和 Compose 混合开发。
接下来的内容是在有 Android 开发的基础之上去讲述的。
二、Composable 概念
方法加上@Composable 注解对应的就是View,也叫做Composable函数,compose中没有传统的View这个概念。
对于需要渲染成界面的Composable函数,也称之为可组合函数 :
- 此函数带有@Composable注解,表明他是一个可组合函数,所有组合函数都必须要带上这个注解
- 可组合函数需要在其他可组合函数的 作用域内 被调用
- 为了与普通函数区分,要求组合函数 首字母大写
- 带上 @Preview注解的Composeable函数,点击desgin选项,可以实时预览样式,不过函数不能带参数,不然预览不了
三、Compose和传统XML混合使用
ComposeView其实是个Android的View。
AndroidView其实是个Composable函数。
AndroidView和ComposeView是采用传统XML的View和Compose组件互相调用的桥梁。
1.XML中使用Compose
XML中写一个ComposeView组件
然后 setContent 一下就好了
2.Compose中使用XML
在Composeable 函数作用域中使用AndroidView
四、单一的Activity管理
Android Compose 中不使用Fragment,而是采用单一Activity管理多个可组合组件( Composable )的架构。
Compose通过 NavController 和 NavHost 实现页面导航,支持动态添加、移除可组合组件,无需传统Fragment即可实现界面切换。
这种架构优化了内存管理和界面渲染效率,避免了Fragment生命周期管理复杂问题。
构建视图的起点就是Activity,控件的使用方法就不细说了。
强制使用Fragment其实也可以,也就是前面说的在Android中使用Compose
五、Compose状态管理和数据流
1.重组(状态)
首先,要抛弃传统 Android View 的命令式思维(找到这个TextView,然后设置它的文本)。
Compose 是声明式的,你只需要声明在某种状态下,UI 应该长什么样。
当状态改变时,Compose 框架会自动重新执行相关的 UI 代码(这个过程叫重组 Recomposition),从而更新界面。
为了让这个过程可预测、易于管理且不易出错,Compose 官方强烈推荐并践行单向数据流(Unidirectional Data Flow, UDF)架构。
- 状态提升(State Hoisting):将状态从子组件提升到共同的父组件,通过参数传递状态和回调函数,实现单向数据流
- remember 和 mutableStateOf:用于在重组过程中保持本地状态,是 Compose 状态管理的基础
交互引起的数据流变化会引起视图的重组。
重组范围是只有关联到的组件会重组。根组件都会重组。
① 基础重组
@Composable
fun RecompositionBasic() {
var count by remember { mutableIntStateOf(0) }
println("RecompositionBasic 被调用 - count: $count") // 观察重组
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "点击次数: $count",
style = MaterialTheme.typography.headlineMedium
)
Button(
onClick = {
count++ // 状态改变触发重组
println("按钮被点击,count 变为: $count")
}
) {
Text("点击我")
}
// 这个组件不依赖 count,不应该重组
StaticComponent()
}
}
@Composable
fun StaticComponent() {
println("StaticComponent 被重组了") // 观察是否被重组
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "我是静态组件,不依赖任何状态",
modifier = Modifier.padding(16.dp)
)
}
}
② 使用key能优化重组
key的作用是为每个列表项提供一个稳定的标识符。Compose 会基于这些稳定的 key 来追踪每个项的状态,并根据需要进行重组。这样,Compose 就可以在数据发生变化时,仅更新有变化的项,而不是重新绘制整个列表。优化视图重用,避免不必要的重组和视图创建。
// 使用 key 优化重组
LazyColumn {
items(
items = items,
key = { item -> item.id } // 使用稳定的 key
) { item ->
}
}
③ 智能重组
- 最小化重组范围:只重组发生变化的部分。
- 智能追踪视图状态:通过唯一的标识符(如 key)和数据绑定,Compose 可以智能地识别哪些视图需要更新,哪些可以保留不变。
// 智能重组 - Compose 跳过不必要的重组
@Composable
fun SmartRecomposition() {
var parentState by remember { mutableStateOf(0) }
println("SmartRecomposition 父组件重组")
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "智能重组",
style = MaterialTheme.typography.headlineLarge
)
Button(onClick = { parentState++ }) {
Text("触发父组件状态变化: $parentState")
}
Spacer(modifier = Modifier.height(16.dp))
// 传递相同的参数,Compose 会跳过重组
ChildComponent(
title = "固定标题", // 稳定参数
value = 100 // 稳定参数
)
Spacer(modifier = Modifier.height(16.dp))
// 传递变化的参数,会触发重组
ChildComponent(
title = "动态标题",
value = parentState // 变化的参数
)
}
}
@Composable
fun ChildComponent(
title: String,
value: Int
) {
println("ChildComponent 重组 - title: $title, value: $value")
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "值: $value",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
2.数据流
核心思想:单向数据流 (UDF)
- 状态向下流动,事件向上流动 (State flows down, events flow up)。
① 状态向下流动 (State Flows Down)
- 状态(State) :驱动 UI 的数据。
- 向下流动:高级别的组件(通常是 ViewModel 或屏幕级别的 Composable 函数)持有并管理状态。它将这些状态作为参数,传递给需要展示这些数据的低级别的子 Composable 函数。
- 子 Composable 只负责接收和展示状态,它自己不应该去修改这个状态。
② 事件向上流动 (Events Flow Up)
- 事件(Event) :用户的操作,比如点击按钮、输入文本等。
- 向上流动:当用户在低级别的 Composable 上进行操作时,这个 Composable 不会直接修改状态。相反,它会触发一个回调函数,这个回调函数是由父组件(或者是 ViewModel)传递给它的。
- 这个事件沿着组件树向上传递,最终到达状态的持有者(ViewModel)。
③ 状态更新
- ViewModel 接收到事件后,根据业务逻辑更新它自己持有的 状态。
- 由于 ViewModel 中的状态(通常是 StateFlow 或 MutableState)发生了变化,Compose 会自动检测到这个变化。
- Compose 触发重组(Recomposition) ,所有读取了这个状态的 Composable 函数会重新执行。
- UI 根据新的状态值,自动刷新成最新的样子。
没看懂?不要着急!接下来会有个例子!
关于Flow,个人的一篇文章:juejin.cn/post/748117…
六、三个层次的架构模式
根据上面说的 Compose 的特点,可以得出三个层次的架构模式。
1.UI 层 (UI Layer)
①UI 元素 (Composable 函数)
也就是视图,用来显示数据(状态)
② 状态持有者 (State Holders)
持有并管理 UI 状态(State)。向 UI 暴露状态(通过 StateFlow)。提供处理 UI 事件(Events)的方法,并在此方法内执行业务逻辑或调用其他层来更新状态。
实现方法: ViewModel。
ViewModel 持有 StateFlow。
Composable 通过 collectAsStateWithLifecycle() 订阅这个 StateFlow,实现状态向下流动。
用户操作触发 Composable 中的事件回调,例如 onClick = { viewModel.refreshNews() },实现事件向上流动。
ViewModel 中的 refreshNews() 方法更新 UiState。
StateFlow 发出新的 UiState,Composable 自动重组并刷新界面。
注意的是这里的ViewModel是分在了UI层面
2.领域层 (Domain Layer)
这一层是可选的。
装复杂的、可在多个 ViewModel 之间复用的业务逻辑。它位于 UI 层和数据层之间,起到一个协调和解耦的作用。
这个具体看下面的例子吧。
3.数据层 (Data Layer)
这是应用所有数据的来源。
提供和管理应用所需的数据。它应该作为单一数据源 (Single Source of Truth) 。
我们熟悉的仓库 (Repository) 就应该放在这里,这是数据层的门面。UI 层和领域层只与 Repository 交互,不关心数据究竟来自网络还是本地数据库。Repository 负责决定从哪里获取数据、何时缓存数据等策略。
七、总结 + 例子
三层架构:
- UI
- 领域
- 数据
目录结构应该是这样子的
activity
ui 也就是 composable 函数就不放出来了
class DebugActivity : AppCompatActivity(){
// Compose 中 viewModel 的标准获取方式
private val viewModel: DebugViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
return setContent {
MaterialTheme{
// 有状态的控件
// 从 ViewModel 中收集状态
val items by viewModel.uiState.collectAsState()
DebugScreen(
items = items.items,
context = this.applicationContext,
// 将 UI 事件委托给 ViewModel
onUrlChanged = viewModel::onUrlChanged,
onGoClicked = viewModel::onGoClicked,
onDebugSwitchChanged = viewModel::onDebugSwitchChanged,
onBackPress = {
finish()
}
)
}
}
}
}
Viewmodel
class DebugViewModel(application: Application) : AndroidViewModel(application) {
// 引入新的状态来表示加载中、成功、失败
data class DebugScreenUiState(
val isLoading: Boolean = true,
var items: List<ListItem> = emptyList(),
val error: String? = null
)
// 数据来源
private val getDebugScreenDataUseCase by lazy {
GetDebugScreenDataUseCase()
}
// 私有的、可变的 StateFlow,作为内部状态持有者 加下划线区别开私有
private val _uiState = MutableStateFlow(DebugScreenUiState())
// asStateFlow 确保 单向数据流 和 封装 特性
// !!!注意重绘的触发逻辑!!!
val uiState: StateFlow<DebugScreenUiState> = _uiState.asStateFlow()
init {
// ViewModel 初始化时加载初始数据
loadInitialData()
}
private fun loadInitialData() {
getDebugScreenDataUseCase(context)
.onSuccess {
_uiState.value = DebugScreenUiState(isLoading = false, items = it)
}
.onFailure {
_uiState.value = DebugScreenUiState(isLoading = false, error = it.message)
}
}
fun onUrlChanged(newUrl: String) {
}
fun onGoClicked(url: String) {
}
fun onDebugSwitchChanged(isEnabled: Boolean) {
}
}
领域层
class GetDebugScreenDataUseCase{
// 这里相当于中转站
// 可以持有多个Repository
private val userRepository by lazy {
UserRepository()
}
operator fun invoke(context: Context): Result<List<ListItem>> {
}
}