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

Flutter Android端非消耗型内购购买状态验证异常排查

问题:Flutter Android端非消耗型内购购买状态检查失败

问题背景

iOS端已成功实现非消耗型内购功能,但Android端出现购买状态验证问题:

  • 产品ID为kaidoku的内购商品配置完成,测试账号设置正常
  • purchase()函数执行成功,二次调用时提示"You already own this item",说明购买流程无问题
  • 启动应用调用check()函数时,无法进入purchaseStream的监听回调,无法获取购买状态

调试输出

I/flutter (12879): Got into check() function
I/flutter (12879): Got out of listening

简化的check()函数

Future<void> check() async {
  final inapp = InAppPurchase.instance;
  print("Got into check() function");
  _subscription = inapp.purchaseStream.listen((data) async {
    print("Got into listening data");
  }, onDone: () {}, onError: (error) {});

  print("Got out of listening");
}

完整InApp类代码

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'globals.dart' as globals;
import 'package:in_app_purchase/in_app_purchase.dart';

String productId = 'kaidoku';

class InApp {
  final Set<String> _productIds = <String>{productId};
  final inapp = InAppPurchase.instance;
  late StreamSubscription _subscription;

  Future<void> check() async {
    final completer = Completer<void>();
    globals.purchaseCancel = false;
    globals.purchaseError = false;
    if (kDebugMode) {
      print("Got into check() function");
    }
    _subscription = inapp.purchaseStream.listen((data) async {
      if (kDebugMode) {
        print("Got into listening data");
      }
      if (data.isNotEmpty) {
        PurchaseDetails purchaseDetails =
            data.firstWhere((purchase) => purchase.productID == productId);

        switch (purchaseDetails.status) {
          case PurchaseStatus.purchased:
            if (purchaseDetails.pendingCompletePurchase) {
              await inapp.completePurchase(purchaseDetails);
            }
            globals.purchased = true;
            if (!completer.isCompleted) {
              completer.complete();
            }
            break;
          case PurchaseStatus.error:
            globals.purchaseError = true;
            globals.purchaseErrorMessage = purchaseDetails.error.toString();
            if (!completer.isCompleted) {
              completer.complete();
            }
            break;
          case PurchaseStatus.pending:
            // Purchase is pending, waiting for update
            break;
          case PurchaseStatus.restored:
            if (purchaseDetails.pendingCompletePurchase) {
              await inapp.completePurchase(purchaseDetails);
            }
            globals.purchased = true;
            if (!completer.isCompleted) {
              completer.complete();
            }
            break;
          case PurchaseStatus.canceled:
            globals.purchaseCancel = true;
            await inapp.completePurchase(purchaseDetails);
            if (!completer.isCompleted) {
              completer.complete();
            }
            break;
        }
      }
    }, onDone: () {
      globals.purchaseCancel = true;
      if (!completer.isCompleted) {
        completer.complete();
      }
      _subscription.cancel();
    }, onError: (error) {
      globals.purchaseError = true;
      globals.purchaseErrorMessage = 'Network connection error.';
      if (!completer.isCompleted) {
        completer.complete();
      }
    });
    if (kDebugMode) {
      print("Got out of listening");
    }
    return completer.future;
  }

  /// purchase
  Future<void> purchase() async {
    if (globals.purchased) {
      return;
    }
    //user will now buy the product
    List<ProductDetails> availableProducts = await _getAvailableProducts();
    if (availableProducts.isNotEmpty) {
      ProductDetails productDetails = availableProducts
          .firstWhere((productDetail) => productDetail.id == productId);
      final PurchaseParam purchaseParam =
          PurchaseParam(productDetails: productDetails);
      await inapp.isAvailable();
      inapp.buyNonConsumable(purchaseParam: purchaseParam);
    }
  }

  /// Restore purchase
  Future<void> restore() async {
    inapp.restorePurchases();
  }

  /// get products
  Future<List<ProductDetails>> _getAvailableProducts() async {
    await inapp.isAvailable();
    ProductDetailsResponse productDetailQueryResponse =
        await inapp.queryProductDetails(_productIds);
    return Future.value(productDetailQueryResponse.error == null
        ? productDetailQueryResponse.productDetails
        : <ProductDetails>[]);
  }
}

原因分析

  1. purchaseStream触发逻辑差异:iOS端会自动同步已购状态到流中,但Android端需要显式调用restorePurchases()才会触发流推送已购数据
  2. check()函数缺少主动触发操作:仅订阅流但未触发恢复购买,流没有事件可推送,因此不会进入回调
  3. 流订阅时机问题:如果先触发恢复再订阅流,会错过恢复操作产生的事件

解决建议

1. 修改check()函数,先订阅流再触发恢复购买

确保流处于监听状态,不会错过恢复操作的事件,同时增加内购服务可用性检查:

Future<void> check() async {
  final completer = Completer<void>();
  globals.purchaseCancel = false;
  globals.purchaseError = false;
  
  // 先检查内购服务是否可用
  bool isAvailable = await inapp.isAvailable();
  if (!isAvailable) {
    globals.purchaseError = true;
    globals.purchaseErrorMessage = "In-app purchase not available";
    completer.complete();
    return completer.future;
  }

  if (kDebugMode) {
    print("Got into check() function");
  }
  
  // 先订阅流
  _subscription = inapp.purchaseStream.listen((data) async {
    if (kDebugMode) {
      print("Got into listening data");
    }
    if (data.isNotEmpty) {
      PurchaseDetails purchaseDetails =
          data.firstWhere((purchase) => purchase.productID == productId);

      switch (purchaseDetails.status) {
        case PurchaseStatus.purchased:
        case PurchaseStatus.restored:
          if (purchaseDetails.pendingCompletePurchase) {
            await inapp.completePurchase(purchaseDetails);
          }
          globals.purchased = true;
          if (!completer.isCompleted) {
            completer.complete();
          }
          break;
        case PurchaseStatus.error:
          globals.purchaseError = true;
          globals.purchaseErrorMessage = purchaseDetails.error.toString();
          if (!completer.isCompleted) {
            completer.complete();
          }
          break;
        case PurchaseStatus.pending:
          break;
        case PurchaseStatus.canceled:
          globals.purchaseCancel = true;
          if (!completer.isCompleted) {
            completer.complete();
          }
          break;
      }
    }
  }, onDone: () {
    if (!completer.isCompleted) {
      completer.complete();
    }
    _subscription.cancel();
  }, onError: (error) {
    globals.purchaseError = true;
    globals.purchaseErrorMessage = 'Network connection error.';
    if (!completer.isCompleted) {
      completer.complete();
    }
  });

  if (kDebugMode) {
    print("Got out of listening");
  }
  
  // 触发恢复购买,让流推送已购数据
  inapp.restorePurchases();
  
  return completer.future;
}

2. 确保completePurchase执行彻底

购买或恢复成功后,必须调用completePurchase标记交易完成,否则Android端可能会重复触发交易事件,导致状态异常

3. 处理流的广播特性(可选)

将purchaseStream转换为广播流,避免多次订阅丢失事件:

final purchaseStream = InAppPurchase.instance.purchaseStream.asBroadcastStream();

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 14:32:33