如何基于领域驱动设计(DDD)实现Flutter的RevenueCat订阅内购包?
用DDD封装Flutter RevenueCat订阅包的实践方案
作为刚接触DDD的开发者,你核心的诉求是隔离第三方SDK的依赖,不让业务代码直接依赖RevenueCat的自定义类(比如Package、Offerings),这完全符合DDD中「领域层独立于外部依赖」的原则。下面是一套落地的步骤和代码示例:
核心思路:端口与适配器模式
我们用DDD常用的**端口与适配器(Ports & Adapters)**来解耦:
- 「端口」:定义领域层需要的抽象(比如获取订阅选项、发起购买的接口),完全不涉及外部SDK
- 「适配器」:实现这些端口,负责对接RevenueCat的SDK,处理外部类和领域模型的转换
第一步:定义领域层的抽象与模型
先把业务关心的订阅信息抽象成自己的领域模型,彻底摆脱对RevenueCat类的依赖:
1. 领域模型:SubscriptionOption
// 领域层模型,只包含业务需要的属性 class SubscriptionOption { final String id; final String title; final String price; final String period; // 比如"monthly"、"annual" // 内部存储RevenueCat的Package标识,仅适配器层使用(领域层不需要知道) final String _rcPackageId; SubscriptionOption({ required this.id, required this.title, required this.price, required this.period, required String rcPackageId, }) : _rcPackageId = rcPackageId; // 给适配器层暴露内部的SDK标识 String get rcPackageId => _rcPackageId; }
2. 仓储端口:SubscriptionRepository
定义领域层需要的操作接口,业务代码只依赖这个抽象:
abstract class SubscriptionRepository { // 获取可用的订阅选项 Future<List<SubscriptionOption>> getSubscriptionOptions(); // 发起订阅购买 Future<void> purchaseSubscription(SubscriptionOption option); }
第二步:实现RevenueCat适配器
创建适配器类,实现上面的SubscriptionRepository,负责和RevenueCat SDK交互,处理模型转换:
import 'package:purchases_flutter/purchases_flutter.dart'; class RevenueCatSubscriptionRepository implements SubscriptionRepository { @override Future<List<SubscriptionOption>> getSubscriptionOptions() async { final offerings = await Purchases.getOfferings(); final currentOffering = offerings.current; if (currentOffering == null) return []; // 把RevenueCat的Package转换成自己的SubscriptionOption return currentOffering.availablePackages.map((rcPackage) { return SubscriptionOption( id: rcPackage.identifier, title: rcPackage.product.title, price: rcPackage.product.priceString, period: _mapPeriod(rcPackage.product.subscriptionPeriod), rcPackageId: rcPackage.identifier, ); }).toList(); } @override Future<void> purchaseSubscription(SubscriptionOption option) async { // 通过领域模型里的rcPackageId找到对应的RevenueCat Package final offerings = await Purchases.getOfferings(); final currentOffering = offerings.current; final rcPackage = currentOffering?.availablePackages.firstWhere( (pkg) => pkg.identifier == option.rcPackageId, ); if (rcPackage != null) { await Purchases.purchasePackage(rcPackage); } else { throw Exception("订阅选项不存在"); } } // 辅助方法:把RevenueCat的周期转换成业务易读的字符串 String _mapPeriod(SubscriptionPeriod period) { switch (period.unit) { case SubscriptionPeriodUnit.month: return period.value == 1 ? "monthly" : "${period.value} months"; case SubscriptionPeriodUnit.year: return period.value == 1 ? "annual" : "${period.value} years"; default: return "unknown"; } } }
第三步:封装UI组件,隔离SDK依赖
现在你的UI层不需要直接使用RevenueCat的PurchaseButton,而是封装自己的组件,依赖领域模型:
1. 自定义SubscriptionButton
class SubscriptionButton extends StatelessWidget { final SubscriptionOption option; final SubscriptionRepository subscriptionRepo; const SubscriptionButton({ Key? key, required this.option, required this.subscriptionRepo, }) : super(key: key); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () async { try { await subscriptionRepo.purchaseSubscription(option); // 处理购买成功逻辑(比如跳转首页、显示提示) } catch (e) { // 处理购买失败逻辑 } }, child: Text("订阅 ${option.title} - ${option.price}"), ); } }
2. 改造UpsellScreen
现在的UpsellScreen完全依赖领域模型和抽象仓储,彻底摆脱RevenueCat的类:
class UpsellScreen extends StatelessWidget { final SubscriptionRepository subscriptionRepo; const UpsellScreen({ Key? key, required this.subscriptionRepo, }) : super(key: key); @override Widget build(BuildContext context) { return FutureBuilder<List<SubscriptionOption>>( future: subscriptionRepo.getSubscriptionOptions(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Scaffold( appBar: AppBar(title: Text("Upsell Screen")), body: Center(child: Text("Loading...")), ); } if (snapshot.hasError || snapshot.data?.isEmpty == true) { return const Scaffold( appBar: AppBar(title: Text("Upsell Screen")), body: Center(child: Text("暂无可用订阅")), ); } final subscriptions = snapshot.data!; return Scaffold( appBar: const AppBar(title: Text("Upsell Screen")), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: subscriptions .map((option) => SubscriptionButton( option: option, subscriptionRepo: subscriptionRepo, )) .toList(), ), ), ); }, ); } }
额外优化建议
- 依赖注入:用
get_it或riverpod这类工具注入SubscriptionRepository的实例,这样后续如果要替换成其他支付SDK,只需要实现新的适配器即可,业务代码完全不用改。 - 错误处理:在领域层定义自己的
SubscriptionFailure异常类,适配器层把RevenueCat的错误转换成领域异常,让业务层更易处理。 - 状态管理:如果用Bloc/Cubit,把订阅相关的逻辑放到Bloc里,UI层只处理状态渲染,进一步分离关注点。
内容的提问来源于stack exchange,提问作者Gerry
相关产品推荐
相关产品推荐

