You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose+Hilt:ViewModel如何向Google Billing Client传Activity?

Jetpack Compose + Billing Library v5:ViewModel中传递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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 16:54:57