Android应用内支付问题:querySkuDetailsAsync返回空列表
Android应用内支付querySkuDetailsAsync返回空列表问题排查
我正尝试在我的Android应用中实现应用内支付功能,用户点击“Remove Ads”菜单按钮后,会初始化BillingClient并处理应用内支付流程。以下是我的实现代码:
public class PlayersActivity extends AppCompatActivity { SharedPreferences prefs; boolean adFree; BillingClient billingClient; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_view); prefs = this.getSharedPreferences("package_name", Context.MODE_PRIVATE); adFree = prefs.getBoolean("adFree", false); if (!adFree) { MobileAds.initialize(this, new OnInitializationCompleteListener() { @Override public void onInitializationComplete(InitializationStatus initializationStatus) { } }); AdView mAdView = findViewById(R.id.listBannerAd); AdRequest adRequest = new AdRequest.Builder().build(); mAdView.loadAd(adRequest); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_history, menu); MenuItem item6 = menu.findItem(R.id.action_remove_ads); if (!adFree) { item6.setVisible(true); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_remove_ads: setUpBillingClient(); return true; } return super.onOptionsItemSelected(item); } private void handlePurchase(Purchase purchase) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { ConsumeParams consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); ConsumeResponseListener listener = (billingResult, purchaseToken) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { prefs.edit().putBoolean("adFree", true).apply(); } }; billingClient.consumeAsync(consumeParams, listener); } } private final PurchasesUpdatedListener purchasesUpdatedListener = (billingResult, purchases) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { for (Purchase purchase : purchases) { handlePurchase(purchase); } } }; private void setUpBillingClient() { Log.v("InAppPurchase", "billingclientsetup"); billingClient = BillingClient.newBuilder(this) .setListener(purchasesUpdatedListener) .enablePendingPurchases() .build(); startConnection(); } private void startConnection() { Log.v("InAppPurchase", "startconnection"); billingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResult) { Log.v("InAppPurchase", "billing setup finished"); if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { queryAvailableProducts(); } } @Override public void onBillingServiceDisconnected() { // Try to restart the connection on the next request to // Google Play by calling the startConnection() method. Log.v("InAppPurchase", "billing disconnected"); } }); } private void queryAvailableProducts() { Log.v("InAppPurchase", "queryavailableproducts"); List<String> skuList = new ArrayList<>(); skuList.add("remove_ads"); SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP); billingClient.querySkuDetailsAsync(params.build(), (billingResult, list) -> { Log.v("InAppPurchases", billingResult.getResponseCode() + "response"); Log.v("InAppPurchases", list + "list"); if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null && !list.isEmpty()) { Log.v("InAppPurchase", list.get(0).getDescription().toString()); BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() .setSkuDetails(list.get(0)) .build(); billingClient.launchBillingFlow(PlayersActivity.this, billingFlowParams); } }); } }但在日志中,我发现querySkuDetailsAsync返回了空列表,响应码为0。我已经尝试了StackOverflow上的一些解决方案,比如确保应用已发布到Google Play商店,且应用内商品在Play Console中配置正确。请问我的代码实现是否存在问题?
先排查querySkuDetailsAsync返回空列表的问题
响应码为0(BillingResponseCode.OK)说明BillingClient和Play服务的连接是正常的,但返回空列表通常和以下几个细节有关:
- SKU ID完全匹配问题:你代码里用的SKU是
"remove_ads",请务必确认Play Console里配置的应用内商品ID和这个字符串完全一致——包括大小写、下划线/连字符、拼写,哪怕一个字符出错都会导致返回空列表。 - 测试账号与发布轨道配置:
- 如果应用已发布到正式轨道,测试支付必须使用Play Console中添加的许可测试账号,普通账号无法看到正式上架的商品;
- 如果用内部测试/封闭测试轨道,要确保测试账号已经加入对应的测试群组,且应用完成了测试轨道的审核(哪怕是内部测试的轻量审核)。
- 商品状态检查:去Play Console的应用内商品页面,确认
remove_ads的状态是活跃(Active),而不是草稿或未发布状态。 - 额外调试日志:可以在
queryAvailableProducts里加一段日志,当列表为空时明确提示,比如Log.v("InAppPurchase", "SKU list is empty, check Play Console configuration"),方便定位问题。
另外,你的代码存在一个严重的逻辑错误!
注意到你在handlePurchase里对remove_ads调用了consumeAsync——但**“移除广告”属于非消耗品(Non-Consumable)**,这类商品是用户永久拥有的,不需要也绝对不能被消耗!
消耗操作是为一次性商品(比如游戏内金币、道具)设计的,消耗后用户可以再次购买。如果对非消耗品执行消耗,会导致:
- 用户购买后,Play商店的购买记录会被标记为已消耗,下次重装应用时
queryPurchases查不到这个购买记录; - 你仅靠本地SharedPreferences存储的
adFree标记,在用户卸载重装后会丢失,用户需要重新购买。
正确的处理方式应该是:
- 应用启动时,主动查询用户已购买的非消耗品,用Play商店的记录覆盖本地状态;
- 处理购买时,不需要调用
consumeAsync,而是调用acknowledgePurchase确认购买(非消耗品必须确认,否则用户可申请退款),然后更新本地标记。
修改后的核心逻辑示例:
// 在onCreate中添加已购商品查询逻辑 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_view); prefs = this.getSharedPreferences("package_name", Context.MODE_PRIVATE); adFree = prefs.getBoolean("adFree", false); // 初始化BillingClient并查询已购商品 setUpBillingClientForPurchaseCheck(); // ... 原广告初始化逻辑不变 } private void setUpBillingClientForPurchaseCheck() { billingClient = BillingClient.newBuilder(this) .setListener(purchasesUpdatedListener) .enablePendingPurchases() .build(); billingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { queryPurchasedNonConsumables(); } } @Override public void onBillingServiceDisconnected() { billingClient.startConnection(this); } }); } private void queryPurchasedNonConsumables() { billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder() .setProductType(BillingClient.SkuType.INAPP) .build(), (billingResult, purchases) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { for (Purchase purchase : purchases) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { handlePurchase(purchase); } } } }); } private void handlePurchase(Purchase purchase) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { if (!purchase.isAcknowledged()) { // 非消耗品必须确认购买,避免用户退款 AcknowledgePurchaseParams acknowledgeParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); billingClient.acknowledgePurchase(acknowledgeParams, billingResult -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { updateAdFreeStatus(true); } }); } else { updateAdFreeStatus(true); } } } private void updateAdFreeStatus(boolean isAdFree) { adFree = isAdFree; prefs.edit().putBoolean("adFree", isAdFree).apply(); // 更新广告和菜单UI AdView mAdView = findViewById(R.id.listBannerAd); mAdView.setVisibility(isAdFree ? View.GONE : View.VISIBLE); if (!isAdFree) { AdRequest adRequest = new AdRequest.Builder().build(); mAdView.loadAd(adRequest); } invalidateOptionsMenu(); }
最后补充几个调试小技巧
- 确保测试设备上的Google Play服务是最新版本;
- 可以用Play Console的静态响应测试功能,使用官方测试SKU(比如
android.test.purchased)验证支付流程是否正常; - 检查日志中
list的打印是否准确,有时候list.toString()显示为[]可能是日志格式问题,可以尝试遍历列表打印每个元素。
内容的提问来源于stack exchange,提问作者Will Zap
相关产品推荐
相关产品推荐

