在 Flutter SDK 中使用付费墙编辑工具启用购买功能

要启用应用内购买,你需要了解三个核心概念:

  • 产品 – 用户可以购买的任何内容(订阅、消耗型商品、永久授权)
  • 流程 – 向用户展示产品的界面序列,通过无代码 Flow Builder 构建。SDK 通过 getFlow 获取它们。如果你更倾向于在自己的代码中构建 UI,请使用付费墙代替 — 参见手动实现付费墙
  • 版位 – 在应用中的何处、何时展示流程(如 mainonboardingsettings)。你在看板中将流程关联到版位,然后在代码中通过版位 ID 请求它们。这样可以轻松运行 A/B 测试,并向不同用户展示不同的流程。 Adapty 为您提供三种在应用中启用购买的方式。请根据您的应用需求选择其中一种: | 实现方式 | 复杂度 | 适用场景 | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Flow Builder | ✅ 简单 | 您在无代码编辑工具中创建完整的、可直接购买的流程。Adapty 会自动渲染,并在后台处理所有复杂的购买流程、收据验证和订阅管理。 | | 手动创建付费墙 | 🟡 中等 | 您在应用代码中自行实现付费墙 UI,但仍从 Adapty 获取流程对象,以保持产品方案的灵活性。请参阅指南。 | | Observer 模式 | 🔴 困难 | 您已有自己的购买处理基础设施,并希望继续使用。请注意,Observer 模式在 Adapty 中存在一定限制。请参阅文章。 |

以下步骤说明如何实现在 Adapty Flow Builder 中创建的流程。

如果您希望自行构建付费墙 UI,请参阅手动实现付费墙

要在您的应用代码中展示 Adapty Flow Builder 中创建的流程,您只需:

  1. 获取流程:从 Adapty 获取流程。
  2. 展示流程,Adapty 将为您处理购买:在您的应用中显示该视图。
  3. 处理按钮操作:将用户交互与您的应用响应关联起来。例如,当用户点击按钮时打开链接或关闭流程。

开始之前

在开始之前,请完成以下步骤:

  1. 在 Adapty 看板中将你的应用连接到 App Store 和/或 Google Play
  2. 在 Adapty 中创建产品
  3. 创建流程并向其添加产品
  4. 创建版位并将流程添加到其中
  5. 在应用代码中安装并激活 Adapty SDK。本指南使用 Adapty Flutter SDK v4 API。

完成这些步骤最快的方式是参照快速入门指南,或使用 Developer CLI 创建付费墙和版位。

1. 获取流程

你的流程与在看板中配置的版位相关联。通过版位,你可以为不同的目标受众运行不同的流程,或进行 A/B 测试

要获取在 Adapty 付费墙编辑工具中创建的流程,你需要:

  1. 使用 getFlow 方法通过版位 ID 获取 flow 对象,并通过 hasViewConfiguration 属性检查该流程是否由编辑工具创建。

  2. 使用 createFlowView 方法创建流程视图。该视图包含显示流程所需的 UI 元素和样式。

要获取视图配置,必须在编辑工具中开启 Show on device 开关。否则将获取到空的视图配置,流程也不会显示。


try {
  // the requested flow
  final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID');
  final view = await AdaptyUI().createFlowView(
    flow: flow,
  );
} on AdaptyError catch (adaptyError) {
  // handle the error
} catch (e) {
  // handle the error
}

2. 展示流程

获取到 flow 视图后,只需添加几行代码即可将其展示出来。

要展示流程,请对由 createFlowView 方法创建的 view 调用 view.present() 方法。每个 view 只能展示一次:关闭后,该视图会从内存中释放。如果需要再次展示流程,请重新调用 createFlowView 以创建新的 view 实例。

try {
  await view.present();
} on AdaptyError catch (e) {
  // handle the error
} catch (e) {
  // handle the error
}

有关如何展示流程的更多详情,请参阅我们的指南

3. 处理按钮操作

当用户点击流程中的按钮时,Flutter SDK 会自动处理购买、恢复购买、关闭视图和打开 URL 等操作。但其他按钮具有自定义或预定义的 ID,需要在代码中处理相应操作。

要控制或监控流程屏幕上的操作,请实现 AdaptyUIFlowsEventsObserver 方法,并在展示任何屏幕之前设置观察者。当用户执行某个操作时,flowViewDidPerformAction 会被触发,您的应用需要根据操作 ID 做出相应响应。 三个观察者方法是必须实现的flowViewDidFinishPurchaseflowViewDidFinishRestoreflowViewDidReceiveError — 缺少这些方法,代码将无法编译。

请参阅我们的指南,了解如何处理按钮操作事件

将观察者实现为一个专用的长生命周期对象,而不是 widget。由于整个应用只有一个全局观察者插槽,将其绑定到 State 会导致页面泄漏(SDK 持有对它的强引用),并且在下一个页面注册自身时会被静默替换。使用 extends 还会继承 SDK 的默认行为,因此除了三个必需方法外,你只需覆盖自己关心的回调即可。

// A dedicated, long-lived handler for flow events.
// It does NOT live inside a Widget/State, so it never leaks and is never
// silently replaced when screens are pushed or popped.
class FlowEventsHandler extends AdaptyUIFlowsEventsObserver {
  // A single, app-wide instance — same idiom as Adapty() and AdaptyUI().
  static final FlowEventsHandler _instance = FlowEventsHandler._();
  factory FlowEventsHandler() => _instance;
  FlowEventsHandler._();

  // This method is called when user performs an action on the flow UI.
  // Overriding it replaces the default behavior (dismiss on close, open URLs),
  // so keep those cases if you want to preserve it.
  @override
  void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) {
    switch (action) {
      case const CloseAction():
      case const AndroidSystemBackAction(): // close the flow on the Android back button
        view.dismiss();
        break;
      case OpenUrlAction(:final url, :final openIn):
        AdaptyUI().openUrl(url, openIn: openIn);
        break;
      default:
        break;
    }
  }

  // Required: decide what happens after a purchase finishes
  @override
  void flowViewDidFinishPurchase(AdaptyUIFlowView view,
      AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) {
    if (purchaseResult is! AdaptyPurchaseResultUserCancelled) {
      view.dismiss();
    }
  }

  // Required: dismiss the flow once a restore succeeds
  @override
  void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) {
    view.dismiss();
  }

  // Required: handle rendering and other view errors
  @override
  void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) {
    print('Flow error: $error');
    view.dismiss();
  }
}

在应用启动时注册一次处理程序,须在任何流程显示之前完成:

AdaptyUI().setFlowsEventsObserver(FlowEventsHandler());

后续步骤

有疑问或遇到问题?欢迎访问我们的支持论坛,在那里你可以找到常见问题的解答,也可以提出自己的问题。我们的团队和社区随时为你提供帮助!

您的付费墙已准备好在应用中显示。在 App Store 沙盒Google Play Store 中测试您的购买,确保可以从付费墙完成测试购买。

现在,您需要检查用户的访问等级,以确保您向正确的用户展示付费墙或授予付费功能的访问权限。

完整示例

下面展示了如何将上述所有步骤整合到你的应用中。


void main() {
  // Register a single, long-lived observer once, before any flow is shown.
  // It is intentionally a plain object (NOT a Widget/State): its lifetime is the
  // whole app, so it never leaks and is never silently replaced when screens are
  // pushed or popped.
  AdaptyUI().setFlowsEventsObserver(FlowEventsHandler());

  runApp(MaterialApp(home: FlowScreen()));
}

/// A dedicated handler for AdaptyUI flow events.
///
/// It `extends` [AdaptyUIFlowsEventsObserver] (rather than being implemented
/// by a `State`), which gives you two things for free:
///   * the SDK's sensible defaults for optional callbacks, so besides the three
///     required methods you only override what you actually care about;
///   * a lifecycle that is independent of the widget tree — there is no strong
///     reference back into a `Widget`, so nothing leaks and there is nothing to
///     unregister.
///
/// Every callback receives the [AdaptyUIFlowView] it relates to, so handling
/// flow actions never requires a `BuildContext` or widget state.
class FlowEventsHandler extends AdaptyUIFlowsEventsObserver {
  // A single, app-wide instance — same idiom as Adapty() and AdaptyUI().
  static final FlowEventsHandler _instance = FlowEventsHandler._();
  factory FlowEventsHandler() => _instance;
  FlowEventsHandler._();

  // Called when the user performs an action on the flow UI.
  @override
  void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) {
    switch (action) {
      case const CloseAction():
      case const AndroidSystemBackAction(): // close the flow on the Android back button
        view.dismiss();
        break;
      case OpenUrlAction(:final url, :final openIn):
        // Open the URL natively, honoring the dashboard browser setting.
        AdaptyUI().openUrl(url, openIn: openIn);
        break;
      default:
        break;
    }
  }

  // Required: decide what happens after a purchase finishes.
  @override
  void flowViewDidFinishPurchase(AdaptyUIFlowView view,
      AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) {
    if (purchaseResult is! AdaptyPurchaseResultUserCancelled) {
      view.dismiss();
    }
  }

  // Required: dismiss the flow once a restore succeeds.
  @override
  void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) {
    view.dismiss();
  }

  // Required: handle rendering and other view errors.
  @override
  void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) {
    print('Flow error: $error');
    view.dismiss();
  }
}

class FlowScreen extends StatefulWidget {
  const FlowScreen({super.key});

  @override
  State<FlowScreen> createState() => _FlowScreenState();
}

class _FlowScreenState extends State<FlowScreen> {
  @override
  void initState() {
    super.initState();
    _showFlowIfNeeded();
  }

  Future<void> _showFlowIfNeeded() async {
    try {
      final flow = await Adapty().getFlow(
        placementId: 'YOUR_PLACEMENT_ID',
      );

      if (!flow.hasViewConfiguration) return;

      final view = await AdaptyUI().createFlowView(flow: flow);

      await view.present();
    } catch (_) {
      // Handle any errors (network, SDK issues, etc.)
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Adapty Flow Example')),
      body: Center(
        // Add a button to re-trigger the flow for testing purposes.
        child: ElevatedButton(
          onPressed: _showFlowIfNeeded,
          child: const Text('Show Flow'),
        ),
      ),
    );
  }
}