Jetpack Compose+Hilt:ViewModel如何向Google Billing Client传Activity?
为什么不推荐层层传递Activity到ViewModel
Google代码实验室里那种把Activity从MainActivity层层传递到ViewModel的方式不是最佳实践:
- ViewModel的设计初衷是独立于UI组件生命周期,持有Activity引用容易引发内存泄漏(比如Activity销毁后ViewModel还未回收,导致Activity实例无法被GC)。
- 这违反了单一职责原则,ViewModel应该专注于数据和业务逻辑,不应该依赖具体的UI容器(Activity)。
推荐的实现方案
方案1:UI层触发计费流程,ViewModel只负责准备参数
把启动计费流程的操作放在Composable中,ViewModel仅负责生成BillingFlowParams并通过State暴露给UI层,UI层通过LocalContext获取Activity来启动流程。
ViewModel代码:
@HiltViewModel class PremiumViewModel @Inject constructor( private val billingClient: BillingClient ) : ViewModel() { private val _pendingBillingParams = MutableStateFlow<BillingFlowParams?>(null) val pendingBillingParams: StateFlow<BillingFlowParams?> = _pendingBillingParams // 准备计费参数,通知UI层启动流程 fun preparePremiumPurchase(skuDetails: SkuDetails) { val params = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .build() _pendingBillingParams.value = params } // 重置状态,避免重复触发 fun clearPendingParams() { _pendingBillingParams.value = null } // 其他计费逻辑:比如查询商品列表、监听购买结果等 }
Composable代码:
@Composable fun PremiumScreen( modifier: Modifier = Modifier, screenTitle: String, viewModel: PremiumViewModel = hiltViewModel(), billingClient: BillingClient = // 通过Hilt注入或全局获取 ) { val context = LocalContext.current val pendingParams by viewModel.pendingBillingParams.collectAsState() // 监听待处理的计费参数,启动流程 pendingParams?.let { params -> viewModel.clearPendingParams() val activity = context as? ComponentActivity activity?.run { billingClient.launchBillingFlow(this, params) } } // UI交互:点击按钮触发ViewModel准备参数 Button( modifier = modifier.padding(16.dp), onClick = { // 假设已通过ViewModel查询到skuDetails viewModel.preparePremiumPurchase(/* 查询到的SkuDetails */) } ) { Text("购买高级会员") } }
方案2:用Activity Result API封装计费流程
把计费流程封装成ActivityResultContract,在Composable中通过rememberLauncherForActivityResult启动,完全不需要ViewModel持有Activity引用,结果直接回调给UI层或ViewModel。
自定义ActivityResultContract:
class BillingFlowContract(private val billingClient: BillingClient) : ActivityResultContract<BillingFlowParams, BillingResult>() { override fun createIntent(context: Context, input: BillingFlowParams): Intent { // 直接在Contract中启动计费流程 val activity = context as ComponentActivity billingClient.launchBillingFlow(activity, input) // 返回空Intent,因为计费结果通过BillingClient的回调监听 return Intent() } override fun parseResult(resultCode: Int, intent: Intent?): BillingResult { // 这里可以根据需要封装结果,实际购买结果建议通过BillingClient的PurchasesUpdatedListener处理 return BillingResult.newBuilder().setResponseCode(resultCode).build() } }
Composable中使用:
@Composable fun PremiumScreen( modifier: Modifier = Modifier, screenTitle: String, viewModel: PremiumViewModel = hiltViewModel(), billingClient: BillingClient = // 注入或获取 ) { val billingLauncher = rememberLauncherForActivityResult(BillingFlowContract(billingClient)) { result -> // 将结果传递给ViewModel处理 viewModel.handleBillingResult(result) } Button(onClick = { val params = BillingFlowParams.newBuilder() .setSkuDetails(/* 查询到的SkuDetails */) .build() billingLauncher.launch(params) }) { Text("立即购买") } }
ViewModel处理结果:
@HiltViewModel class PremiumViewModel @Inject constructor() : ViewModel() { fun handleBillingResult(result: BillingResult) { // 根据响应码更新UI状态:比如显示成功提示、失败弹窗等 when (result.responseCode) { BillingClient.BillingResponseCode.OK -> { // 购买成功逻辑 } BillingClient.BillingResponseCode.USER_CANCELED -> { // 用户取消购买 } else -> { // 其他错误处理 } } } }
方案3:Hilt注入Activity(不推荐)
如果确实需要在ViewModel中直接调用launchBillingFlow,可以通过Hilt的@ActivityContext注解注入Context,再转成Activity,但必须用WeakReference避免内存泄漏。
ViewModel代码:
@HiltViewModel class PremiumViewModel @Inject constructor( private val billingClient: BillingClient, @ActivityContext private val context: Context ) : ViewModel() { // 用弱引用持有Activity,避免内存泄漏 private val activityRef = WeakReference(context as ComponentActivity) fun launchPremiumPurchase(params: BillingFlowParams) { activityRef.get()?.let { activity -> billingClient.launchBillingFlow(activity, params) } } }
注意:这种方案会让ViewModel依赖UI组件,违反架构设计原则,仅在特殊场景下使用。
总结
最佳实践是让ViewModel专注于业务逻辑,UI层负责与Activity相关的操作:通过State或Activity Result API实现ViewModel和UI层的通信,避免ViewModel持有Activity引用,既符合架构设计规范,又能有效避免内存泄漏问题。
内容的提问来源于stack exchange,提问作者StackerSapper

