3. Compose 项目结构设计:模块划分、支付业务层封装、状态管理方案

3.1 模块划分原则与分层架构

在集成支付宝支付 SDK 的 Compose 项目中,合理的模块划分决定了后续业务扩展、单元测试与团队协作的效率。本课程采用经典的 分层架构,将项目拆解为四个核心模块:

模块名称职责描述依赖关系
:app壳工程,管理依赖注入、Application 初始化、路由注册。依赖所有下游模块
:core:network封装 OkHttp / Retrofit,提供统一的网络请求与响应拦截。无项目内依赖
:core:payment支付业务层,桥接支付宝 SDK,提供统一支付调用接口。依赖 :core:network
:feature:checkoutCompose UI 层,包含支付页面、订单确认页、回调展示。依赖 :core:payment
模块划分避坑:
- 避免将支付宝 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 项目各模块之间的依赖方向以及支付数据流的完整路径:

Compose 项目模块划分与支付数据流 :app 壳工程 / DI :feature:checkout Compose UI / State :core:payment 支付桥接层 :core:network 网络请求层 支付数据流向 ① 用户点击支付 ② ViewModel 调用 PaymentRepository ③ PaymentGateway 桥接 ④ 支付宝 SDK 回调 ⑤ StateFlow -> UI 更新 状态管理方案 Idle / Loading Success / Error Sealed Class + StateFlow

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 "确认支付")
    }
}
实践要点:
- 使用 build.gradle.kts 中的 api / implementation 严格控制模块依赖暴露。
- 支付桥接层应提供 Mock 实现,便于 UI 层独立测试。
- 状态管理中的 Sealed Class 尽量不加 data 以外的字段,避免跨版本兼容问题。