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

如何基于领域驱动设计(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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 09:17:43