in_app_purchase_platform_interface 版本演进全解析:从接口定义到支付落地实践
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
本篇文章以 Flutter 官方维护的in_app_purchase_platform_interface包(当前版本 1.4.1)的 CHANGELOG.md 为主线,逐版本梳理其 API 演进脉络,并结合仓库源码深入讲解getCountryCode、PurchaseStatus.canceled、currencySymbol、completePurchase等关键能力的设计动机与正确用法。读完本文,你将掌握该平台接口包的核心抽象(InAppPurchasePlatform与InAppPurchasePlatformAddition)、各版本 API 变动的实际影响,以及如何正确实现"购买—发货—完成交易"的完整支付闭环。
一、这个包是什么:支付插件的"契约层"
in_app_purchase_platform_interface是in_app_purchase插件的公共平台接口(platform interface),它的职责正如 README.md 所述:让in_app_purchase插件本身以及各平台实现(Android、iOS 等)都遵循同一套接口约定,从而保证上层 API 与底层商店(Google Play、App Store)实现解耦。
从 pubspec.yaml 可以看到它的核心依赖只有两个:
flutterSDK(当前要求 Flutter >= 3.38.0,Dart SDK ^3.10.0,对应 CHANGELOG 中 1.4.1 版本"Flutter 3.35/Dart 3.9"的最低 SDK 声明);plugin_platform_interface: ^2.1.7(1.3.7 版本中升级到该最低版本,用于提供PlatformInterface.verify校验机制)。
包的topics元数据声明了in-app-purchase与payment两个话题(对应 1.3.5 版本"Adds pub topics to package metadata"),便于在 pub.dev 上被检索。
接口层的两种扩展方式
平台接口包在设计上强烈倾向非破坏性变更(README 中明确强调 "Strongly prefer non-breaking changes"),这是它演进的核心原则。插件使用者可以有两种方式扩展能力:
- 实现标准 API:继承抽象类
InAppPurchasePlatform(lib/src/in_app_purchase_platform.dart),并在注册时调用InAppPurchasePlatform.setInstance(MyPlatformInAppPurchase())。文档特别提醒:应使用extends而非implements,因为接口新增方法时,extends的子类会继承默认实现而不会编译报错,而implements的实现类会被新增方法破坏。 - 实现平台特有能力:对于标准 API 未覆盖的平台专属功能,继承
InAppPurchasePlatformAddition(lib/src/in_app_purchase_platform_addition.dart),注册时设置InAppPurchasePlatformAddition.instance = MyPlatformInAppPurchaseAddition()。
二、从 1.0.0 到 1.4.1:版本演进脉络
下表汇总了 CHANGELOG.md 中所有版本的要点:
| 版本 | 核心变更 |
|---|---|
| 1.4.1 | 最低 SDK 提升到 Flutter 3.35/Dart 3.9;在 README 与 docstring 中澄清completePurchase用法及未完成交易的后果 |
| 1.4.0 | 新增getCountryCodeAPI;最低 SDK 提升到 Flutter 3.13/Dart 3.1 |
| 1.3.7 | plugin_platform_interface最低版本提升到 2.1.7;最低 SDK 提升到 Flutter 3.10/Dart 3.0 |
| 1.3.6 | 文档引用从finishPurchase更正为completePurchase |
| 1.3.5 | 新增 pub topics 元数据;最低 SDK 提升到 Flutter 3.7/Dart 2.19 |
| 1.3.4 | 移除对非空值多余的 null 检查;最低 Flutter 版本提升到 3.3;对齐 Dart/Flutter SDK 约束 |
| 1.3.3 | 更新 flutter/plugins 合并进 flutter/packages 后的链接;最低 Flutter 版本提升到 3.0 |
| 1.3.2 | 调整 import 以符合prefer_relative_imports规范;最低 Flutter 版本提升到 2.10;移除多余 import |
| 1.3.1 | 改用plugin_platform_interface2.1.0 引入的verify方法 |
| 1.3.0 | 新增PurchaseStatus.canceled枚举值,用于区分"出错"与"用户主动取消" |
| 1.2.0 | 为IAPError新增toString() |
| 1.1.0 | ProductDetails新增currencySymbol字段 |
| 1.0.1 | 修复"恢复历史购买"(Restoring previous purchases)链接 |
| 1.0.0 | 首个开源发布 |
从这份演进历史可以清晰看到平台接口包的三种典型变更类型:SDK 约束升级(几乎每个版本都在跟进 Flutter/Dart 最低版本)、文档与命名澄清(finishPurchase→completePurchase、恢复购买链接修复)、以及面向实际业务痛点的 API 新增(canceled状态、currencySymbol、countryCode)。下面重点展开这三个业务向变更。
三、1.3.0:用PurchaseStatus.canceled区分"错误"与"用户取消"
在 1.3.0 之前,用户取消支付流程会与真正的支付失败一起落入error状态,开发者无法在 UI 上区分"用户主动放弃"与"支付过程出错",导致取消场景被当作错误上报,影响统计与提示文案。
1.3.0 引入的PurchaseStatus.canceled解决了这一问题。完整枚举定义见 lib/src/types/purchase_status.dart:
enum PurchaseStatus { pending, // 支付流程进行中,可提示用户等待 purchased, // 支付成功完成,应发放商品 error, // 支付过程出错,流程中止 restored, // 购买已恢复到设备(跨设备恢复场景) canceled, // 用户取消了购买 }在 UI 层如何区分处理
PurchaseStatus是PurchaseDetails.status的类型(见 lib/src/types/purchase_details.dart),实际使用时典型的 UI 处理逻辑为:
switch (purchaseDetails.status) { case PurchaseStatus.purchased: // 校验票据 -> 发放商品 -> completePurchase break; case PurchaseStatus.restored: // 校验票据 -> 恢复商品 -> completePurchase break; case PurchaseStatus.error: // 展示错误信息(通过 purchaseDetails.error) break; case PurchaseStatus.canceled: // 静默关闭弹窗或提示"已取消",不当作错误上报 break; case PurchaseStatus.pending: // 展示等待中的加载态 break; }值得注意的是,PurchaseDetails中的error字段(类型IAPError?)仅在状态为error时非空,transactionDate仅在状态为purchased时非空,源码注释对此有明确说明。
四、1.1.0:ProductDetails.currencySymbol与价格展示本地化
价格展示是支付场景中容易踩坑的细节:ProductDetails原本已有price(格式化价格字符串,如"$0.99")和rawPrice(数值型价格)与currencyCode(ISO 4217 货币代码),但缺少适合当前 locale 的货币符号。
1.1.0 在 lib/src/types/product_details.dart 中为ProductDetails新增了currencySymbol字段:
class ProductDetails { // ... final String price; // 已格式化的价格,如 "$0.99" final double rawPrice; // 纯数值价格,如 0.99(以整货币单位描述) final String currencyCode; // ISO 4217 货币代码,如 "USD" final String currencySymbol; // 新增:locale 对应货币符号,如 "$" }源码注释明确了currencySymbol的取值规则:返回当前 locale 下的货币符号(如美区为$);当无法确定货币符号时,回退返回 ISO 4217 货币代码。这使开发者可以在不依赖额外本地化库的情况下,直接拼接出符合用户地区习惯的价格文案。
五、1.4.0:getCountryCode—— 面向合规与定价的新 API
1.4.0 在InAppPurchasePlatform抽象类中新增了countryCode()方法(对应 CHANGELOG 中的getCountryCodeAPI)。其声明见 lib/src/in_app_purchase_platform.dart:
/// Returns the user's country. /// /// Android: /// Returns Play billing country code based on ISO-3166-1 alpha2 format. /// /// iOS: /// Returns the country code from SKStoreFrontWrapper. Future<String> countryCode() => throw UnimplementedError('countryCode() has not been implemented.');该 API 的平台语义
从源码 docstring 可以提取出两个平台各自的实现依据:
- Android:返回基于 ISO-3166-1 alpha2 格式的 Play 结算国家代码,对应 Android
BillingClient的BillingConfig; - iOS:返回来自 StoreKit
SKStoreFront的国家代码。
典型应用场景
countryCode常用于:根据用户所在国家/地区展示差异化定价或汇率提示、判断税务与合规要求、以及针对不同地区做 A/B 定价实验。由于各平台获取该信息的时机与格式存在差异(Android 为 ISO-3166-1 alpha2,iOS 来自 StoreKit StoreFront),上层在使用时应做好空值兜底与缓存策略。
需要说明的是,新增方法同时遵循了本包的演进原则:InAppPurchasePlatform的子类若使用extends,会自然继承throw UnimplementedError(...)的默认实现,只有实现类主动覆盖该方法才能获得真实国家代码——这正是平台接口包"默认抛错、按需实现"的标准模式。
六、1.4.1 与completePurchase:为什么"完成交易"如此关键
1.4.1 版本没有新增 API,但在 README 与 docstring 中显著澄清了completePurchase的用法与未完成交易的后果,这实际上是对开发者最容易踩坑的问题的一次"文档级修复"。对应实现见 lib/src/in_app_purchase_platform.dart。
必须完成的交易:两条平台的铁律
源码中的> [!WARNING]块给出了非常具体的平台行为差异:
- iOS/macOS:如果不对某笔交易调用
completePurchase,该交易会一直停留在 Apple 的 unfinished transaction queue(未完成交易队列)中,带来两个直接后果:- 每次应用重启,该交易都会在
purchaseStream上被反复重新投递; - 之后再次购买同一产品 ID 会失败,报错信息为"存在待处理的重复交易"(duplicate transaction is pending)。
- 每次应用重启,该交易都会在
- Android:如果不对交易调用
completePurchase,Google Play 会在3 天后自动退款并撤销该购买。
正确的完成时机
completePurchase的调用时机不是"支付成功时",而是**"商品已发货之后"**。PurchaseDetails中的pendingCompletePurchase字段(见 lib/src/types/purchase_details.dart)正是为辅助判断而设计:当它为true且商品已交付给用户时,开发者必须调用InAppPurchasePlatform.completePurchase完成收尾。
需要特别注意的是:
- 每个状态为
purchased或restored的PurchaseDetails都有责任被completePurchase; - 对
pending状态的购买调用completePurchase会抛出异常; completePurchase失败时会抛出PurchaseException,应根据errorCode决定是立即重试、稍后重试,还是修复应用代码/商店配置问题。
为什么购买结果要走 Stream 而不是返回值
buyConsumable与buyNonConsumable的返回值只是bool(表示购买请求是否初始发送成功),真正的购买结果全部通过purchaseStream异步送达。这一设计在源码 docstring 中有清晰阐述:购买可能由应用内触发,也可能由用户在商店前台(store front)直接触发,还可能是跨设备恢复的购买;甚至上一会话未完成的购买会在新会话启动时重新投递。因此 purchaseStream 的文档 强烈建议:在应用启动的最早阶段(最好在main()返回主 Widget 之前)订阅该广播流,否则会错过订阅之前发生的购买更新;同时建议同一时刻只保持一个订阅。
七、其他版本变更:质量与规范的持续打磨
除业务 API 外,CHANGELOG 记录了大量工程质量层面的变更,理解它们有助于判断升级成本:
- 1.3.1(
verify机制):改用plugin_platform_interface2.1.0 引入的verify方法。在 lib/src/in_app_purchase_platform.dart 的instancesetter 中可以看到PlatformInterface.verify(instance, _token)的调用——它通过私有 token 校验传入实例确实继承自本抽象类,防止把不相关对象误注册为平台实现,这是plugin_platform_interface提供的核心安全机制。 - 1.3.2 / 1.3.4 / 1.3.5:属于代码风格与约束对齐,包括
prefer_relative_imports导入规范、移除对非空值的多余 null 检查、对齐 Dart 与 Flutter SDK 约束、新增 pub topics。 - 1.3.6:将文档引用从历史遗留的
finishPurchase统一更正为completePurchase,避免开发者被旧命名误导。 - 1.2.0(
IAPError.toString()):为错误对象增加字符串表示,便于日志记录与调试,对应 lib/src/errors/in_app_purchase_error.dart。
八、包结构速览与进一步阅读
in_app_purchase_platform_interface的库入口 lib/in_app_purchase_platform_interface.dart 统一导出以下模块:
src/errors/errors.dart:IAPError、IAPException(PurchaseException等错误体系,测试见 test/src/errors/);src/in_app_purchase_platform.dart:核心抽象类InAppPurchasePlatform(上述全部标准 API 的声明处);src/in_app_purchase_platform_addition.dart与_addition_provider.dart:平台特有功能的扩展机制;src/types/types.dart:统一导出ProductDetails、ProductDetailsResponse、PurchaseDetails、PurchaseParam、PurchaseStatus、PurchaseVerificationData等类型(见 lib/src/types/types.dart)。
若要深入实现层面,可以继续阅读:
- in_app_purchase 主包:面向应用开发者的高层 API;
- in_app_purchase_android 与 in_app_purchase_storekit:分别基于 Google Play Billing 与 Apple StoreKit 的平台实现,是理解
countryCode、completePurchase等接口在各端落地细节的最佳参考; - in_app_purchase_platform_interface 测试:通过 mock 验证平台实现注册与接口调用流程。
九、升级与使用建议
结合 CHANGELOG 的版本节奏,给出以下实践建议:
- 升级前先核对 SDK 约束:1.4.x 系列要求 Flutter 3.35+/Dart 3.9+,1.3.x 系列要求 Flutter 3.10+/Dart 3.0+,升级包的同时需同步升级 Flutter 工具链;
- 取消场景务必处理
canceled状态:使用 1.3.0+ 后,不要再把用户取消当作error处理,否则会产生错误埋点与误导性提示; - 价格展示使用
currencySymbol兜底:1.1.0+ 可直接取currencySymbol,无法确定时它会回退为货币代码,无需再自行做符号映射; - 交易完成是硬性义务:无论平台,都要在商品交付后调用
completePurchase,否则在 iOS 上会阻塞后续购买、在 Android 上会触发 3 天自动退款——这是 1.4.1 文档澄清最想传达的信息; - 尽早订阅
purchaseStream:在main()早期订阅,避免错过恢复购买与上次会话遗留交易的投递。
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考