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

如何为依赖Google Billing ProductDetails的Composable编写预览?

解决Compose预览中无法实例化私有构造类的问题

问题背景

现有一个Compose组件定义如下:

@Composable
fun PurchaseSubscriptionScreenContent(
    availableSubscriptions: List<ProductDetails>,
    selectedSubscription: ProductDetails,
    isLoading: Boolean,
    onPurchaseClicked: () -> Unit,
) {...}

编写预览时,由于ProductDetails的构造方法为私有,直接实例化会报错:Cannot access '<init>': it is package-private in 'ProductDetails'。不想在预览中调用Google Billing联网API,也觉得将组件参数替换为基础类型的方案过于繁琐,寻求更优实现方式。

更优解决方案

方案1:接口抽象解耦(推荐长期维护)

通过抽象UI所需的核心属性为接口,让组件依赖抽象而非具体的ProductDetails类型,实现业务逻辑与UI的解耦,同时方便预览时构造模拟数据。

  1. 定义抽象接口,包含UI需要的所有属性:
interface SubscriptionInfo {
    val productId: String
    val displayName: String
    val formattedPrice: String
    val description: String
}
  1. 修改原有组件,依赖抽象接口:
@Composable
fun PurchaseSubscriptionScreenContent(
    availableSubscriptions: List<SubscriptionInfo>,
    selectedSubscription: SubscriptionInfo,
    isLoading: Boolean,
    onPurchaseClicked: () -> Unit,
) {
    // 原有UI逻辑,改用SubscriptionInfo的属性
}
  1. 业务层实现适配器,将ProductDetails转换为接口实例:
class GoogleSubscriptionAdapter(private val product: ProductDetails) : SubscriptionInfo {
    override val productId: String = product.productId
    override val displayName: String = product.name
    override val formattedPrice: String = 
        product.subscriptionOfferDetails?.firstOrNull()?.pricingPhase?.formattedPrice ?: ""
    override val description: String = product.description
}
  1. 预览时直接使用自定义数据类实现接口:
data class PreviewSubscription(
    override val productId: String,
    override val displayName: String,
    override val formattedPrice: String,
    override val description: String
) : SubscriptionInfo

@Preview(showBackground = true)
@Composable
fun PurchaseSubscriptionScreenPreview() {
    val previewSubscriptions = listOf(
        PreviewSubscription(
            productId = "monthly",
            displayName = "月度会员",
            formattedPrice = "¥19.9",
            description = "每月解锁全部高级功能"
        ),
        PreviewSubscription(
            productId = "yearly",
            displayName = "年度会员",
            formattedPrice = "¥199",
            description = "立省38.8元,全年畅享所有权益"
        )
    )
    PurchaseSubscriptionScreenContent(
        availableSubscriptions = previewSubscriptions,
        selectedSubscription = previewSubscriptions.first(),
        isLoading = false,
        onPurchaseClicked = {}
    )
}

方案2:Mock库生成实例(快速实现预览)

使用MockK或Mockito等Mock库绕过私有构造限制,直接生成ProductDetails的模拟实例,无需修改原有组件结构。

以MockK为例:

  1. 确保项目中引入MockK依赖(仅需在debug/test环境引入)
  2. 编写预览代码:
@Preview(showBackground = true)
@Composable
fun PurchaseSubscriptionScreenMockPreview() {
    // 模拟月度订阅
    val mockMonthlySub = mockk<ProductDetails> {
        every { productId } returns "monthly"
        every { name } returns "月度会员"
        every { description } returns "每月解锁全部高级功能"
        every { subscriptionOfferDetails } returns listOf(mockk {
            every { pricingPhase } returns mockk {
                every { formattedPrice } returns "¥19.9"
            }
        })
    }
    // 模拟年度订阅
    val mockYearlySub = mockk<ProductDetails> {
        every { productId } returns "yearly"
        every { name } returns "年度会员"
        every { description } returns "立省38.8元,全年畅享所有权益"
        every { subscriptionOfferDetails } returns listOf(mockk {
            every { pricingPhase } returns mockk {
                every { formattedPrice } returns "¥199"
            }
        })
    }
    PurchaseSubscriptionScreenContent(
        availableSubscriptions = listOf(mockMonthlySub, mockYearlySub),
        selectedSubscription = mockMonthlySub,
        isLoading = false,
        onPurchaseClicked = {}
    )
}

方案3:反射生成实例(不推荐)

通过Java/Kotlin反射机制绕过私有构造创建ProductDetails实例,但这种方式依赖类的内部实现,一旦Google Billing库更新可能失效,仅作为临时应急方案:

@Preview
@Composable
fun PurchaseSubscriptionScreenReflectionPreview() {
    val constructor = ProductDetails::class.java.declaredConstructors.first()
    constructor.isAccessible = true
    val mockSub = constructor.newInstance(/* 根据构造方法参数传入对应值 */) as ProductDetails
    // 后续设置属性(可能需要额外反射)
    // ...
    PurchaseSubscriptionScreenContent(
        availableSubscriptions = listOf(mockSub),
        selectedSubscription = mockSub,
        isLoading = false,
        onPurchaseClicked = {}
    )
}

方案对比

  • 接口抽象:解耦UI与第三方依赖,代码可维护性高,适合长期项目
  • Mock库:无需修改原有组件,快速实现预览,适合临时调试或小型项目
  • 反射:风险高,易随库更新失效,仅应急使用

内容的提问来源于stack exchange,提问作者James Olrog

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 20:55:41