在 Flutter SDK 中通过 Flow 和付费墙编辑工具启用购买功能

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

  • 产品 – 用户可购买的任何内容(订阅、消耗型商品、永久授权)
  • 流程与付费墙 – 向用户展示产品的页面,通过无代码的流程与付费墙编辑工具构建,SDK 通过 getFlow 获取它们。如果你更倾向于用自己的代码构建 UI,请参阅实现自定义付费墙。
  • 版位 – 在应用中展示流程与付费墙的位置和时机(例如 main、onboarding、settings)。你可以在看板中将它们关联到版位,然后在代码中通过版位 ID 来请求。这样可以轻松进行 A/B 测试,并向不同用户展示不同内容。

Adapty 为您提供三种在应用中启用购买的方式。请根据您的应用需求选择其中一种:

实现方式复杂度适用场景
Adapty Flow 与付费墙编辑工具✅ 简单你在无代码编辑工具中创建完整的、可直接购买的流程或付费墙。Adapty 自动渲染界面,并在后台处理所有复杂的购买逻辑、收据验证和订阅管理。
自定义付费墙🟡 中等你在应用代码中自行实现付费墙 UI,但仍从 Adapty 获取流程对象,以保持产品方案的灵活性。请参阅指南。
观察者模式🔴 复杂你已有自己的购买处理基础设施,并希望继续使用它。请注意,观察者模式在 Adapty 中存在一定限制。请参阅文章。
Important

以下步骤介绍如何在应用中展示通过 Adapty 流程与付费墙编辑工具创建的流程或付费墙。

如果您希望自行构建付费墙 UI,请参阅实现自定义付费墙。

要展示通过 Adapty 流程与付费墙编辑工具创建的流程或付费墙,只需在应用代码中完成以下操作:

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

开始之前

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

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

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

1. 获取流程或付费墙

你的流程和付费墙与在看板中配置的版位相关联。通过版位,你可以针对不同的目标受众展示不同的流程或付费墙,也可以运行 A/B 测试。

要获取在 Adapty Flow & Paywall Builder 中创建的流程或付费墙,你需要:

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

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

Important

要获取视图配置,请发布流程或付费墙。包含未发布编辑内容的流程或付费墙状态为 Dirty,其版位将继续使用上次发布的版本。


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. 展示流程或付费墙

现在,当你获得流程视图后,只需添加几行代码即可将其展示出来。

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

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

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

3. 处理按钮操作

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

要控制或监听流程屏幕上的操作,请实现 AdaptyUIFlowsEventsObserver 方法,并在展示任何屏幕之前设置观察者。当用户执行某个操作时,flowViewDidPerformAction 将被调用,您的应用需要根据操作 ID 作出响应。

三个观察者方法是必须实现的:flowViewDidFinishPurchase、flowViewDidFinishRestore 和 flowViewDidReceiveError — 缺少这些方法,代码将无法编译。

Tip

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

将观察者实现为一个专用的长生命周期对象,而不是 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());

下一步

Tip

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

您的流程或付费墙已准备好在应用中展示。请在 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'),
        ),
      ),
    );
  }
}