3. Compose 项目结构设计:模块划分、支付业务层封装、状态管理方案
3.1 模块划分原则与分层架构
在集成支付宝支付 SDK 的 Compose 项目中,合理的模块划分决定了后续业务扩展、单元测试与团队协作的效率。本课程采用经典的 分层架构,将项目拆解为四个核心模块:
| 模块名称 | 职责描述 | 依赖关系 |
|---|---|---|
:app | 壳工程,管理依赖注入、Application 初始化、路由注册。 | 依赖所有下游模块 |
:core:network | 封装 OkHttp / Retrofit,提供统一的网络请求与响应拦截。 | 无项目内依赖 |
:core:payment | 支付业务层,桥接支付宝 SDK,提供统一支付调用接口。 | 依赖 :core:network |
:feature:checkout | Compose UI 层,包含支付页面、订单确认页、回调展示。 | 依赖 :core:payment |
模块划分避坑:
- 避免将支付宝 SDK 直接依赖在
- 不要在
- 避免将支付宝 SDK 直接依赖在
:app 模块;必须隔离到 :core:payment 中,便于后续替换为其他支付渠道。- 不要在
:feature 模块中直接引用支付宝 SDK 的原生类(如 PayTask),必须通过桥接层调用。
3.2 支付业务层封装:桥接模式实现
支付业务层 :core:payment 的核心任务是将支付宝 SDK 的 黑盒 API 转化为 Compose 可感知的 挂起函数 / Flow。我们采用桥接模式(Bridge Pattern),将“支付调用”与“支付结果处理”解耦:
// 1. 定义统一支付接口(Abstraction)
interface PaymentGateway {
suspend fun pay(request: PayRequest): PayResult
}
// 2. 支付宝具体实现(ConcreteImplementor)
class AlipayGateway @Inject constructor(
private val context: Context
) : PaymentGateway {
override suspend fun pay(request: PayRequest): PayResult {
return suspendCancellableCoroutine { cont ->
val payTask = PayTask(context)
// 支付宝同步调用,必须放在 IO 线程
val result = payTask.payV2(request.orderInfo, true)
val payResult = PayResult(result.toMap())
if (cont.isActive) {
cont.resume(payResult)
}
}
}
}
// 3. 支付仓库层(使用方)
class PaymentRepository @Inject constructor(
private val gateway: PaymentGateway
) {
suspend fun executeOrder(order: Order): Flow<PaymentState> = flow {
emit(PaymentState.Loading)
val result = gateway.pay(order.toPayRequest())
if (result.isSuccess) {
emit(PaymentState.Success(result.signData))
} else {
emit(PaymentState.Error(result.memo))
}
}
}
重要:支付宝
payV2 方法是同步阻塞调用,必须在 Dispatchers.IO 执行。如果在 Main 线程直接调用会导致 ANR。上述代码使用 suspendCancellableCoroutine + 协程上下文已隐含线程切换,但建议在仓库层显式指定:.flowOn(Dispatchers.IO)。
3.3 Compose 状态管理方案:单向数据流
在 Compose 侧,我们采用 状态提升 + ViewModel + Sealed Class 的三层状态管理方案,确保 UI 与支付逻辑的边界清晰:
// 1. 定义密封状态类
sealed class PaymentState {
object Idle : PaymentState()
object Loading : PaymentState()
data class Success(val sign: String) : PaymentState()
data class Error(val message: String) : PaymentState()
}
// 2. ViewModel 持有状态与驱动动作
@HiltViewModel
class CheckoutViewModel @Inject constructor(
private val repository: PaymentRepository
) : ViewModel() {
private val _paymentState = MutableStateFlow<PaymentState>(PaymentState.Idle)
val paymentState: StateFlow<PaymentState> = _paymentState.asStateFlow()
fun startPayment(order: Order) {
viewModelScope.launch {
repository.executeOrder(order).collect { state ->
_paymentState.value = state
}
}
}
}
// 3. Compose 层订阅状态
@Composable
fun CheckoutScreen(viewModel: CheckoutViewModel = hiltViewModel()) {
val state by viewModel.paymentState.collectAsState()
when (state) {
is PaymentState.Idle -> { /* 显示支付按钮 */ }
is PaymentState.Loading -> { LoadingIndicator() }
is PaymentState.Success -> { PaymentSuccessDialog(state.sign) }
is PaymentState.Error -> { PaymentErrorView(state.message) }
}
}
3.4 模块依赖与数据流向 SVG
下面的 SVG 图展示了 Compose 项目各模块之间的依赖方向以及支付数据流的完整路径:
3.5 常见错误与稳定性优化
3.5.1 循环依赖导致编译失败
若 :core:payment 反向依赖了 :feature:checkout(例如为了发送回调),会出现循环依赖。解决方式:在 :core:payment 中仅定义接口,用 事件总线 或 回调注入 将结果抛出。
3.5.2 支付状态丢失
当 Activity 因配置变更重建时,StateFlow 应保存在 ViewModel 中(ViewModel 通过 Hilt 保持作用域)。如果使用 rememberSaveable 保存支付订单信息,确保序列化正确。
// 避免将 PayResult 整个对象存入 SavedStateHandle
// 只保存必要的 orderId 与 resultCode
@HiltViewModel
class CheckoutViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: PaymentRepository
) : ViewModel() {
private val orderId = savedStateHandle.getStateFlow("orderId", "")
}
3.5.3 性能优化:避免多次 collect
当 Feature 模块包含多个 Composable 时,确保每个 Composable 只 collectAsState() 一次,且使用 remember 避免重组:
@Composable
fun PaymentButton(viewModel: CheckoutViewModel) {
val isLoading by viewModel.paymentState
.map { it is PaymentState.Loading }
.collectAsState(false)
Button(onClick = { viewModel.startPayment(order) }, enabled = !isLoading) {
Text(if (isLoading) "支付中..." else "确认支付")
}
}
实践要点:
- 使用
- 支付桥接层应提供 Mock 实现,便于 UI 层独立测试。
- 状态管理中的
- 使用
build.gradle.kts 中的 api / implementation 严格控制模块依赖暴露。- 支付桥接层应提供 Mock 实现,便于 UI 层独立测试。
- 状态管理中的
Sealed Class 尽量不加 data 以外的字段,避免跨版本兼容问题。