Enable purchases with Flow Builder in Flutter SDK
To enable in-app purchases, you need to understand three key concepts:
- Products – anything users can buy (subscriptions, consumables, lifetime access)
- Flows – screen sequences that present products to users, built in the no-code Flow Builder. The SDK retrieves them via
getFlow. If you’d rather build the UI in your own code, use a paywall instead — see Implement paywalls manually. - Placements – where and when you show flows in your app (like
main,onboarding,settings). You attach flows to placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different flows to different users.
Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements:
| Implementation | Complexity | When to use |
|---|---|---|
| Adapty Flow Builder | ✅ Easy | You create a complete, purchase-ready flow in the no-code builder. Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. |
| Manually created paywalls | 🟡 Medium | You implement your paywall UI in your app code, but still get the flow object from Adapty to maintain flexibility in product offerings. See the guide. |
| Observer mode | 🔴 Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the article. |
The steps below show how to implement a flow created in the Adapty Flow Builder.
If you’d rather build the paywall UI yourself, see Implement paywalls manually.
To display a flow created in the Adapty Flow Builder, in your app code, you only need to:
- Get the flow: Get it from Adapty.
- Display it and Adapty will handle purchases for you: Show the view in your app.
- Handle button actions: Associate user interactions with your app’s response to them. For example, open links or close the flow when users click buttons.
Before you start
Before you start, complete these steps:
- Connect your app to the App Store and/or Google Play in the Adapty Dashboard.
- Create your products in Adapty.
- Create a flow and add products to it.
- Create a placement and add your flow to it.
- Install and activate the Adapty SDK in your app code. This guide uses Adapty Flutter SDK v4 APIs.
The fastest way to complete these steps is to follow the quickstart guide or create paywalls and placements using the Developer CLI.
1. Get the flow
Your flows are associated with placements configured in the dashboard. Placements allow you to run different flows for different audiences or to run A/B tests.
To get a flow created in the Adapty Flow Builder, you need to:
-
Get the
flowobject by the placement ID using thegetFlowmethod and check whether it was created in the builder using thehasViewConfigurationproperty. -
Create the flow view using the
createFlowViewmethod. The view contains the UI elements and styling needed to display the flow.
To get the view configuration, you must switch on the Show on device toggle in the builder. Otherwise, you will get an empty view configuration, and the flow won’t be displayed.
import 'package:adapty_flutter/adapty_flutter.dart';
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. Display the flow
Now, when you have the flow view, it’s enough to add a few lines to display it.
To display the flow, use the view.present() method on the view created by the createFlowView method. Each view can only be presented once: after you dismiss it, the view is released from memory. If you need to display the flow again, call createFlowView one more time to create a new view instance.
try {
await view.present();
} on AdaptyError catch (e) {
// handle the error
} catch (e) {
// handle the error
}
For more details on how to display a flow, see our guide.
3. Handle button actions
When users click buttons in the flow, the Flutter SDK automatically handles purchases, restoration, closing the view, and opening URLs. However, other buttons have custom or pre-defined IDs and require handling actions in your code.
To control or monitor processes on the flow screen, implement the AdaptyUIFlowsEventsObserver methods and set the observer before presenting any screen. If a user has performed some action, the flowViewDidPerformAction will be invoked, and your app needs to respond depending on the action ID.
Three observer methods are required: flowViewDidFinishPurchase, flowViewDidFinishRestore, and flowViewDidReceiveError — your class won’t compile without them.
Implement the observer as a dedicated, long-lived object rather than a widget. Because a single global observer slot is shared across the whole app, binding it to a State would leak the screen (the SDK keeps a strong reference to it) and would be silently replaced when the next screen registers itself. Using extends also inherits the SDK’s default behavior, so besides the three required methods you only override the callbacks you care about.
// 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();
}
}
Register the handler once at app start, before any flow is shown:
AdaptyUI().setFlowsEventsObserver(FlowEventsHandler());
Next steps
Have questions or running into issues? Check out our support forum where you can find answers to common questions or ask your own. Our team and community are here to help!
Your flow is ready to be displayed in the app. Test your purchases in the App Store sandbox or in Google Play Store to make sure you can complete a test purchase from the flow.
Now, you need to check the users’ access level to ensure you display a flow or give access to paid features to right users.
Full example
Here is how all those steps can be integrated in your app together.
import 'package:flutter/material.dart';
import 'package:adapty_flutter/adapty_flutter.dart';
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'),
),
),
);
}
}