"计算机科学中的所有问题都可以通过增加一个 间接层 来解决" ------ David Wheeler
设计目标
- 层次清晰 :结构遵循
UI → ViewModel → Repository → SDK/Backend
,并尽可能精炼。 - 解耦:将数据和 UI 逻辑遵循同样细的粒度拆分。
- 数据和控制单向流动:数据从 Data 流向 UI,控制则反之。
- 状态收束:避免在每一层都引入状态变量。
支付状态流转
支付状态机图
节点表示当时的支付状态,箭头文字则表明状态流转的路径。
PaymentResult.XXX
表示支付 SDK 返回值。ApiResult.XXX
是查询业务后台的结果。
stateDiagram-v2
[*] --> Idle
Idle --> Created: createOrder()
Created --> Paying: startPayment()
Paying --> Confirming: PaymentResult.Success
Paying --> Cancelled: PaymentResult.Cancel
Paying --> Failed: PaymentResult.Failure
Confirming --> Paid: ApiResult.Success
Confirming --> Failed: ApiResult.Failure
Paid --> [*]
Failed --> [*]
Cancelled --> [*]
支付状态机密封类
根据上述状态机图,借助密封类进行实现。
kotlin
// 用密封类对边界进行约束
sealed interface OrderState {
data object Idle : OrderState
data object Created : OrderState
data object Paying : OrderState
data object Confirming : OrderState // 支付完成后轮询状态
data object Paid : OrderState // 已落单
data class Failed(val reason: String) : OrderState
data object Cancelled : OrderState
}
Repository 统一管理支付状态
Repository 将支付 SDK 的状态和订单后台的状态进行统一收束后,只暴露一个 StateFlow
(state
)。
- DataSource :集成支付 SDK 和订单后台,这两者相当于
DataSource
的角色。 - 对外发射 state :唯一对外暴露的状态,统一通过
reduce
改变状态。 - 使用封装后的 PaymentSdk :可以看到
sdk.pay()
函数也使用了Flow
管理状态,将在下文进一步说明。
kotlin
class OrderRepository(
private val paymentSdk: PaymentSdkWrapper,
private val api: OrderApi
) {
private val _state = MutableStateFlow<OrderState>(OrderState.Idle)
val state: StateFlow<OrderState> = _state.asStateFlow()
suspend fun createOrder(params: Map<String, Any>) {
reduce(OrderState.Created)
// 可以在这里调用 api.createOrder(...)
}
suspend fun startPayment(orderId: String) {
reduce(OrderState.Paying)
paymentSdk.pay(orderId)
.catch { e -> reduce(OrderState.Failed(e.message ?: "Unknown error")) }
.collect { result ->
when (result) {
PaymentResult.Success -> confirmOrder(orderId)
PaymentResult.Cancel -> reduce(OrderState.Cancelled)
is PaymentResult.Failure -> reduce(OrderState.Failed(result.reason))
}
}
}
private suspend fun confirmOrder(orderId: String) {
reduce(OrderState.Confirming)
when (val r = api.confirmOrder(orderId)) {
is ApiResult.Success -> reduce(OrderState.Paid)
is ApiResult.Failure -> reduce(OrderState.Failed(r.message))
}
}
private fun reduce(newState: OrderState) {
_state.value = newState
println("OrderState -> $newState") // 方便调试日志
}
}
支付 SDK 及订单后台 API 封装
将支付 SDK 原始的 异步回调
封装为 同步 Flow
方式,利用接口隐藏底层细节,支持替换具体实现。
kotlin
// 支付 SDK 支付结果
sealed interface PaymentResult {
data object Success : PaymentResult
data object Cancel : PaymentResult
data class Failure(val reason: String) : PaymentResult
}
interface PaymentSdkWrapper {
fun pay(orderId: String): Flow<PaymentResult>
}
sealed interface ApiResult {
data object Success : ApiResult
data class Failure(val message: String) : ApiResult
}
interface OrderApi {
suspend fun confirmOrder(orderId: String): ApiResult
}
ViewModel 层:映射为 UIState
ViewModel 只做两件事:
- 把
Repository
状态映射成UIState
:隐藏支付状态机细节,只提供终端用户关心的 UI 信息。 - 对外暴露启动支付的入口。
kotlin
data class PaymentUiState(
val isLoading: Boolean = false,
val message: String? = null,
val isSuccess: Boolean = false
)
class PaymentViewModel(
private val repo: OrderRepository
) : ViewModel() {
val uiState: StateFlow<PaymentUiState> = repo.state
.map { domain ->
when (domain) {
OrderState.Idle -> PaymentUiState()
OrderState.Created -> PaymentUiState(isLoading = true, message = "Order created")
OrderState.Paying -> PaymentUiState(isLoading = true, message = "Paying...")
OrderState.Confirming -> PaymentUiState(isLoading = true, message = "Confirming...")
OrderState.Paid -> PaymentUiState(isSuccess = true, message = "Payment successful")
is OrderState.Failed -> PaymentUiState(message = "Failed: ${domain.reason}")
OrderState.Cancelled -> PaymentUiState(message = "Payment cancelled")
}
}
.stateIn(viewModelScope, SharingStarted.Eagerly, PaymentUiState()) // ===> 解释见下文
fun createOrder(params: Map<String, Any>) {
viewModelScope.launch { repo.createOrder(params) }
}
fun pay(orderId: String) {
viewModelScope.launch { repo.startPayment(orderId) }
}
}
对 .stateIn(viewModelScope, SharingStarted.Eagerly, PaymentUiState()
这段代码的详细说明如下:
- stateIn:将冷流 Flow 转换为热流 StateFlow,不经订阅就可以启动,并且能记住最新值,当订阅者来到时,可以立即获取到最新值。(与之对应,冷流每一次订阅都会从0开始启动收集)
- viewModelScope:将流与 VM 生命周期绑定,VM 销毁时,流也自动取消收集。
- Eagerly :定义了从什么时候开始收集上游 Flow 的策略,常见策略有
Eagerly
:ViewModel 一创建就立刻开始收集。WhileSubscribed(...)
:只有当 UI 层订阅时才收集(比较省资源)。Lazily
:第一次订阅时才开始。
- PaymentUiState():初始值。
UI 层:订阅状态并渲染
最后是负责将 UIState 投射为界面状态的 UI 层,这一层只对状态进行渲染,也会支持一些动画、弹窗、视觉效果等,但决不要掺杂业务逻辑。
kotlin
class PaymentActivity : AppCompatActivity() {
private val viewModel: PaymentViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
if (state.isLoading) showLoading(state.message)
else hideLoading()
state.message?.let { showToast(it) }
if (state.isSuccess) navigateToSuccessPage()
}
}
}
// 举例触发
viewModel.createOrder(mapOf("amount" to 100))
viewModel.pay("order-123")
}
}
交互/UI 视角的状态流程图
描述用户视角的页面状态变化,相比于支付状态机,本图只保留用户能够感知到的状态,并隐藏后台验证等实现细节。可用于向 产品经理、设计师 讲解代码逻辑。
stateDiagram-v2
[*] --> Idle: 页面初始化
Idle --> Loading: 点击下单
Loading --> AwaitingPayment: 订单创建成功
Loading --> Error: 订单创建失败
AwaitingPayment --> Paying: 用户跳转支付
Paying --> Success: 支付成功并确认
Paying --> Failed: 支付失败
Paying --> Cancelled: 用户取消
Error --> [*]
Cancelled --> [*]
Success --> [*]
Failed --> [*]