Migrate Adapty Flutter SDK to v. 4.0
Adapty Flutter SDK 4.0 introduces flows and renames the paywall APIs accordingly. The new APIs work with both the new Flow Builder and the existing Paywall Builder — no setup changes are required on the Adapty Dashboard side.
Quick reference
| v3 | v4 |
|---|---|
Adapty().getPaywall(placementId: id) | Adapty().getFlow(placementId: id) |
Adapty().getPaywallForDefaultAudience(placementId: id) | Adapty().getFlowForDefaultAudience(placementId: id) |
Adapty().getPaywallProducts(paywall: paywall) | Adapty().getPaywallProducts(flow: flow) |
Adapty().logShowPaywall(paywall: paywall) | Adapty().logShowFlow(flow: flow) |
AdaptyPaywall (type) | AdaptyFlow |
AdaptyPaywallFetchPolicy (type) | AdaptyFlowFetchPolicy |
AdaptyUI().createPaywallView(paywall: paywall) | AdaptyUI().createFlowView(flow: flow) |
AdaptyUIPaywallView (type) | AdaptyUIFlowView |
AdaptyUIPaywallPlatformView (widget) | AdaptyUIFlowPlatformView |
AdaptyUI().presentPaywallView(view) / dismissPaywallView(view) | AdaptyUI().presentFlowView(view) / dismissFlowView(view) |
AdaptyUIPaywallsEventsObserver | AdaptyUIFlowsEventsObserver |
AdaptyUI().setPaywallsEventsObserver(observer) | AdaptyUI().setFlowsEventsObserver(observer) |
paywallViewDid* callbacks | flowViewDid* callbacks |
paywallViewDidFailRendering | flowViewDidReceiveError |
AdaptyPaywallProduct keeps its name — products still belong to a flow, and getPaywallProducts now takes an AdaptyFlow. You no longer pass a locale when fetching a flow. The purchase and profile APIs (makePurchase, restorePurchases, getProfile, identify, and so on) are unchanged, and so are the view methods present, dismiss, and showDialog. Some default behaviors changed — see Default behavior changes.
Minimum versions
Adapty Flutter SDK 4.0 raises the minimum requirements:
- iOS 15.0 — the minimum iOS deployment target, raised from iOS 13.0.
- Xcode 26 or newer — the native iOS SDK uses Swift tools 6.2.
- Flutter 3.32.0 (Dart 3.8.0) or newer.
Installation
Update the package
Which package you install depends on whether your app uses Kids Mode.
For most apps, update adapty_flutter to v4.0 in your pubspec.yaml:
dependencies:
adapty_flutter: 4.0.0
If your app uses Kids Mode, specify adapty_flutter_kids instead:
dependencies:
adapty_flutter_kids: 4.0.0
This self-contained package strips out IDFA and ad-tracking code to comply with App Store requirements. Update the Dart import path to package:adapty_flutter_kids/adapty_flutter.dart. Otherwise, the migration is exactly the same as the regular package.
Kids Mode also requires you to disable IP address collection in the Adapty Dashboard — see Kids Mode for the full setup.
iOS: native SDKs now come through Swift Package Manager
CocoaPods’ spec repo goes read-only in December 2026, so starting with v4 the native iOS SDK is no longer distributed through CocoaPods — the plugin pulls it through Swift Package Manager only.
If you are on Flutter 3.32–3.43, enable Swift Package Manager support once:
flutter config --enable-swift-package-manager
Flutter 3.44 and later enables Swift Package Manager by default, so no action is needed there.
Fetching flows
getPaywall → getFlow
The returned type changes from AdaptyPaywall to AdaptyFlow, and you no longer pass a locale — when you render a flow, the localization is resolved automatically; for custom paywalls, all configured locales are returned in flow.remoteConfigs:
- final paywall = await Adapty().getPaywall(placementId: 'YOUR_PLACEMENT_ID', locale: 'en');
+ final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID');
getPaywallForDefaultAudience is renamed the same way:
- final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID', locale: 'en');
+ final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID');
The fetch policy type is renamed from AdaptyPaywallFetchPolicy to AdaptyFlowFetchPolicy; its options (reloadRevalidatingCacheData, returnCacheDataElseLoad, returnCacheDataIfNotExpiredElseLoad) are unchanged.
getPaywallProducts(paywall) → getPaywallProducts(flow)
getPaywallProducts keeps its name but now takes an AdaptyFlow via the flow parameter:
- final products = await Adapty().getPaywallProducts(paywall: paywall);
+ final products = await Adapty().getPaywallProducts(flow: flow);
Data model
getFlow returns an AdaptyFlow instead of an AdaptyPaywall, and the object shape changed:
v3 AdaptyPaywall member | v4 AdaptyFlow member | Action |
|---|---|---|
remoteConfig (single, nullable) | remoteConfigs (list) | A flow carries one remote config per configured language. The remoteConfig getter still exists and returns the first entry; to pick a specific language, search remoteConfigs by its locale. |
productIdentifiers | productIdentifiers | Kept, but now collected across all paywall variations of the flow. Per-variation identifiers live on flow.paywalls[i].productIdentifiers. |
hasViewConfiguration | hasViewConfiguration | Unchanged. |
placementId (deprecated) | removed | Use flow.placement.id. |
revision (deprecated) | removed | Use flow.placement.revision. |
vendorProductIds (deprecated) | removed | Use productIdentifiers. |
| (new) | paywalls (list of AdaptyFlowPaywall) | Each entry is one paywall variation in the flow, with its own name, variationId, and productIdentifiers. |
AdaptyPaywallViewConfiguration is no longer exposed — the view configuration is now opaque. Remove any references to this type.
Web paywall methods
openWebPaywall and createWebPaywallUrl keep their names, but the paywall parameter now takes an AdaptyFlowPaywall (a flow variation) instead of an AdaptyPaywall. You can still pass an AdaptyPaywallProduct instead.
final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID');
- await Adapty().openWebPaywall(paywall: paywall);
+ if (flow.paywalls.isNotEmpty) {
+ await Adapty().openWebPaywall(paywall: flow.paywalls[0]);
+ }
Tracking flow views
logShowPaywall → logShowFlow
logShowPaywall is renamed to logShowFlow and now takes an AdaptyFlow. The event is still logged against the same variation, so existing funnel and A/B test metrics continue to work without dashboard changes.
- await Adapty().logShowPaywall(paywall: paywall);
+ await Adapty().logShowFlow(flow: flow);
As in v3, you do not need to call this method when displaying flows or paywalls rendered by the Flow Builder or the Paywall Builder — Adapty tracks those views automatically.
Displaying flows
createPaywallView → createFlowView
Rename the method and pass the AdaptyFlow via the flow parameter. The other parameters (loadTimeout, preloadProducts, customTags, customTimers, customAssets, productPurchaseParams) are unchanged, and so are the view methods present, dismiss, and showDialog:
- final view = await AdaptyUI().createPaywallView(paywall: paywall);
+ final view = await AdaptyUI().createFlowView(flow: flow);
await view.present();
AdaptyUIPaywallView → AdaptyUIFlowView
The view type is renamed. Its deprecated paywallVariationId property is removed — use variationId:
- void flowViewDidAppear(AdaptyUIPaywallView view) {
+ void flowViewDidAppear(AdaptyUIFlowView view) {
AdaptyUIPaywallPlatformView → AdaptyUIFlowPlatformView
If you embed the view as a widget in your widget tree, rename it and pass the flow parameter. The event callbacks (onDidAppear, onDidFinishPurchase, and so on) keep their names:
- AdaptyUIPaywallPlatformView(
- paywall: paywall,
+ AdaptyUIFlowPlatformView(
+ flow: flow,
onDidFinishPurchase: (view, product, purchaseResult) { /* … */ },
)
A flow view created with createFlowView is single-use: after you call dismiss(), the view is released from memory and can no longer be re-presented — call createFlowView again to present the flow once more.
Handling events
The observer class is renamed from AdaptyUIPaywallsEventsObserver to AdaptyUIFlowsEventsObserver, its registration method from setPaywallsEventsObserver to setFlowsEventsObserver, and all paywallViewDid* callbacks to flowViewDid*:
- class MyObserver extends AdaptyUIPaywallsEventsObserver {
+ class MyObserver extends AdaptyUIFlowsEventsObserver {
@override
- void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) {
+ void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) {
// …
}
}
- AdaptyUI().setPaywallsEventsObserver(this);
+ AdaptyUI().setFlowsEventsObserver(this);
Three callbacks are now required — your observer won’t compile without them:
flowViewDidFinishPurchase: Was optional in v3, where the default dismissed the view after a purchase. Now you decide what happens: continue the flow or callview.dismiss().flowViewDidFinishRestore: Required, as in v3.flowViewDidReceiveError: ReplacespaywallViewDidFailRenderingand now also receives other view errors.
Two smaller changes:
setFlowsEventsObserver(andsetOnboardingsEventsObserver) now acceptnullto detach a previously set observer, so the SDK no longer retains it.- The new optional
flowViewDidReceiveAnalyticEventcallback is reserved for custom analytic events from a flow. Flows don’t emit these to your code yet, so you don’t need to implement it.
v4 also adds capabilities you can opt into:
AdaptyUI().setObserverModeResolver(...)with anAdaptyUIObserverModeResolver— drive purchases and restores initiated from flows while the SDK runs in Observer mode. Previously this was available only in the native iOS and Android SDKs. See Present flows in Observer mode.AdaptyUI().setSystemRequestsHandler(...)with anAdaptyUISystemRequestsHandler— reserved for system requests from a flow (OS permission prompts and App Store review requests). Flows don’t trigger these requests yet, so you don’t need to register a handler.
Removed APIs
These symbols were deprecated in 3.x and are removed in v4:
setFallbackPaywalls → setFallback
- await Adapty().setFallbackPaywalls(assetId);
+ await Adapty().setFallback(assetId);
withIdfaCollectionDisabled → withAppleIdfaCollectionDisabled
configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY')
- ..withIdfaCollectionDisabled(true),
+ ..withAppleIdfaCollectionDisabled(true),
Other removed members
AdaptyPurchaseResultSuccess.jwsTransaction: UseappleJwsTransaction.AdaptyUIFlowView.paywallVariationId: UsevariationId.AdaptyUIObserverandAdaptyUI().setObserver(...): UseAdaptyUIFlowsEventsObserverandsetFlowsEventsObserver(...).
Default behavior changes
These changes do not cause compile errors, so test them at runtime:
- Successful purchase: In v3, the default
paywallViewDidFinishPurchasedismissed the view. In v4,flowViewDidFinishPurchaseis required and has no default — dismiss the view yourself if that’s the behavior you want. - Android system back button: It no longer dismisses a flow by default. The action is delivered to
flowViewDidPerformActionasAndroidSystemBackAction— handle it there if you want the back button to close the flow. - URL opening: The default
flowViewDidPerformActionnow handlesOpenUrlActionby opening the URL natively (honoring the in-app or external browser setting from the dashboard), in addition to dismissing the view onCloseAction. Override the callback to handle URLs yourself. - View errors:
flowViewDidReceiveErroris required, and dismissal is up to your implementation. If your v3 integration relied on the view being dismissed automatically on rendering errors, callview.dismiss()in this callback. - View lifecycle: Dismissing a flow or onboarding view releases it from memory. A dismissed view can no longer be re-presented — create a new one instead.
Onboarding API deprecation
The legacy onboarding API is deprecated in v4.0 in favor of the Flow Builder. It still works, and your IDE flags the deprecated symbols through their @Deprecated annotations — there are no runtime warnings. These symbols will be removed in a future release, so plan migration of your onboardings to the Flow Builder.
Deprecated symbols: getOnboarding, getOnboardingForDefaultAudience, createOnboardingView, presentOnboardingView, dismissOnboardingView, setOnboardingsEventsObserver, AdaptyOnboarding, AdaptyUIOnboardingView, AdaptyUIOnboardingPlatformView, AdaptyUIOnboardingsEventsObserver, and the onboarding state, input, and analytics models.