Migrate Adapty Kotlin Multiplatform SDK to v4.1

Adapty Kotlin Multiplatform SDK 4.1 is the first stable release of the 4.x line — 4.0 was released only as a beta, so if you’re on 3.x, migrate straight to 4.1. This guide covers the whole migration: the flows introduced in 4.0 and the 4.1 changes on top of them.

The 4.x line introduces flows and renames the paywall APIs accordingly. The new APIs work with both the Flow & Paywall Builder and the old Paywall Builder — no setup changes are required on the Adapty Dashboard side. On top of that, 4.1 makes Adapty Attribution opt-in, renames the external attribution APIs and the product subscription type, and adds App Store promoted in-app purchases.

Quick reference

v3v4.1
Adapty Attribution enabled automaticallydisabled by default — opt in with .withAdaptyAttributionEnabled(true)
Adapty.getPaywall(placementId, locale)Adapty.getFlow(placementId)
Adapty.getPaywallForDefaultAudience(placementId, locale)Adapty.getFlowForDefaultAudience(placementId)
Adapty.getPaywallProducts(paywall)Adapty.getPaywallProducts(flow)
Adapty.logShowPaywall(paywall)Adapty.logShowFlow(flow)
AdaptyPaywallAdaptyFlow
AdaptyUI.createPaywallView(paywall, ...)AdaptyUI.createFlowView(flow, ...)
AdaptyUI.createNativePaywallView(...)AdaptyNativePaywallViewAdaptyUI.createNativeFlowView(...)AdaptyNativeFlowView
AdaptyUIPaywallViewAdaptyUIFlowView
AdaptyUI.presentPaywallView(view) / dismissPaywallView(view)AdaptyUI.presentFlowView(view) / dismissFlowView(view)
AdaptyUI.setPaywallsEventsObserver(observer)AdaptyUI.setFlowsEventsObserver(observer)
AdaptyUI.registerPaywallEventsListener / unregisterPaywallEventsListenerAdaptyUI.registerFlowEventsListener / unregisterFlowEventsListener
AdaptyUIPaywallsEventsObserverAdaptyUIFlowsEventsObserver
AdaptyUIPaywallPlatformView(paywall, ...)AdaptyUIFlowPlatformView(flow, ...)
paywallViewDidPerformAction, paywallViewDidAppear, and other paywallView... callbacksflowViewDidPerformAction, flowViewDidAppear, and other flowView... callbacks
paywallViewDidFailRenderingflowViewDidReceiveError
Adapty.updateAttribution(attribution, source) with a String sourceAdapty.updateExternalAttribution(attribution, provider) with an AdaptyExternalAttributionProvider
provider passed as a string, for example "adjust"AdaptyExternalAttributionProvider, for example AdaptyExternalAttributionProvider.ADJUST
AdaptyProfile.appliedAttributionSources: List<String>AdaptyProfile.appliedExternalAttributionProviders: List<AdaptyExternalAttributionProvider>
AdaptyPaywallProductSubscriptionAdaptyProductSubscription
Promoted in-app purchases completed automatically, with no way to intercept themOnPromotedPurchaseListener and Adapty.makePromotedPurchase(product) hand completion to your app

AdaptyPaywallProduct keeps its name — products still belong to a flow, and getPaywallProducts keeps its name too, now taking an AdaptyFlow. The getFlow and getFlowForDefaultAudience methods no longer take a locale parameter — pass it to createFlowView instead. The purchase and profile APIs (makePurchase, restorePurchases, getProfile, identify, updateProfile) and setFallback keep the same signatures, but the fallback file itself must be re-downloaded — see Fallback files. The onboarding methods still work but are deprecated — see Onboarding API deprecation. Some default behaviors changed — see Default behavior changes.

Installation

Update the version and sync the project:

[versions]
adapty-kmp = "<the latest SDK version>"

[libraries]
adapty-kmp = { module = "io.adapty:adapty-kmp", version.ref = "adapty-kmp" }
adapty-kmp-ui = { module = "io.adapty:adapty-kmp-ui", version.ref = "adapty-kmp" }

The adapty-kmp-ui module is only needed if you render flows and paywalls with the Compose Multiplatform layer (view.present()). See Install Adapty SDK for the full setup.

The underlying native Adapty SDKs are bumped to their 4.x releases on both platforms and are resolved automatically — no build changes are needed. The iOS deployment target stays 15.0, unchanged by this release.

⚠️ Adapty Attribution is disabled by default

Warning

If you use Adapty Attribution and update to SDK 4.1 without opting in, it breaks silently — installs stop registering, and nothing warns you.

In earlier versions, the SDK registered installs for Adapty Attribution automatically. Starting from SDK version 4.1, this is off by default: the SDK doesn’t register installs, the listener set with setOnInstallationDetailsListener never fires, and getCurrentInstallationStatus returns AdaptyInstallationStatus.Determined.NotAvailable.

If you use Adapty Attribution, enable it when activating the SDK:

  val config = AdaptyConfig
      .Builder("PUBLIC_SDK_KEY")
+     .withAdaptyAttributionEnabled(true)
      .build()

  Adapty.activate(configuration = config)

If you don’t use Adapty Attribution, no changes are needed.

Fetching flows

getPaywall → getFlow

The returned type changes from AdaptyPaywall to AdaptyFlow, and the locale parameter moves from the fetch call to createFlowView; for custom paywalls, all locales are returned in flow.remoteConfigs:

- Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en")
-     .onSuccess { paywall ->
-         // use the paywall
+ Adapty.getFlow("YOUR_PLACEMENT_ID")
+     .onSuccess { flow ->
+         AdaptyUI.createFlowView(flow = flow, locale = "en")
      }
      .onError { error ->
          // handle the error
      }

locale stays optional on createFlowView: omit it and the view renders in en, or in the flow’s default localization when the flow has no en. See Localizations and locale codes.

getPaywallForDefaultAudience is renamed the same way:

- Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", locale = "en")
+ Adapty.getFlowForDefaultAudience("YOUR_PLACEMENT_ID")

getPaywallProducts(paywall) → getPaywallProducts(flow)

getPaywallProducts keeps its name but now takes an AdaptyFlow:

- Adapty.getPaywallProducts(paywall)
+ Adapty.getPaywallProducts(flow)
      .onSuccess { products ->
          // use the products
      }

Fallback files

The fallback file format changed in SDK v4. Download the new file from Placements > Fallbacks and bundle it in your app.

Data model

getFlow returns an AdaptyFlow instead of an AdaptyPaywall, and the object shape changed:

v3 AdaptyPaywall propertyv4 AdaptyFlow propertyAction
remoteConfig: AdaptyRemoteConfig? (single)remoteConfigs: List<AdaptyRemoteConfig>A flow carries one remote config per configured language. Read the one that matches the user: flow.remoteConfigs.firstOrNull { it.locale == "en" }.
(new)paywalls: List<AdaptyFlowPaywall>Each entry is one paywall variation in the flow, with its own name, variationId, and productIdentifiers. The web paywall methods take an AdaptyFlowPaywall — see Web paywall methods.
productIdentifiersmovedProduct identifiers now live on each variation: flow.paywalls[i].productIdentifiers. To fetch products, keep calling getPaywallProducts(flow).
hasViewConfigurationkeptReports whether the flow ships a layout AdaptyUI can render. It was absent in the 4.0 beta and is back in 4.1 — if you removed your checks for the beta, you can use it again. false means the flow carries no layout, so treat it as remote-config only. You can also call createFlowView and handle the error (see Displaying flows).

hasViewConfiguration is also on AdaptyOnboarding, unchanged.

Web paywall methods

openWebPaywall and createWebPaywallUrl keep their names, but the paywall parameter is replaced by a flowPaywall parameter taking an AdaptyFlowPaywall — one of the variations in flow.paywalls. You can still pass an AdaptyPaywallProduct instead:

- Adapty.openWebPaywall(paywall = paywall)
+ flow.paywalls.firstOrNull()?.let { flowPaywall ->
+     Adapty.openWebPaywall(flowPaywall = flowPaywall)
+ }

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.

- Adapty.logShowPaywall(paywall)
+ Adapty.logShowFlow(flow)

As in v3, you do not need to call this method when displaying flows or paywalls rendered by Adapty — Adapty tracks those views automatically.

Displaying flows

createPaywallView → createFlowView

Rename the factory method and pass the AdaptyFlow. The returned view type is renamed from AdaptyUIPaywallView to AdaptyUIFlowView, but its methods (present, dismiss) and the optional parameters (loadTimeout, preloadProducts, customTags, customTimers, customAssets, productPurchaseParams) are unchanged. One optional parameter is new: locale, which replaces the locale you used to pass to getPaywall — see Fetching flows.

Note

customTimers still exists, but it only affects legacy Paywall Builder paywalls. A flow’s countdown timer runs on the behavior set in the Flow & Paywall Builder, so a flow ignores whatever you pass here.

- AdaptyUI.createPaywallView(paywall)
+ AdaptyUI.createFlowView(flow)
      .onSuccess { view ->
          view.present()
      }
      .onError { error ->
          // handle the error
      }

If you don’t use Compose Multiplatform, the native factory method is renamed the same way:

- AdaptyUI.createNativePaywallView(paywall)
+ AdaptyUI.createNativeFlowView(flow)

createFlowView returns an AdaptyResult.Error if the flow has no view configured, so you can drop the v3 hasViewConfiguration check and handle the error instead:

- if (paywall.hasViewConfiguration) {
-     AdaptyUI.createPaywallView(paywall)
-         .onSuccess { view -> view.present() }
- }
+ AdaptyUI.createFlowView(flow)
+     .onSuccess { view -> view.present() }
+     .onError { error ->
+         // the flow has no view configured, or view creation failed
+     }
Note

A flow view is single-use: after you call dismiss(), the view is destroyed, so call createFlowView again to present the flow once more.

Handling events

The events observer is renamed from AdaptyUIPaywallsEventsObserver to AdaptyUIFlowsEventsObserver, and its callbacks change the paywallView prefix to flowView. Existing handler bodies don’t need code changes — just rename the type and the overrides:

- AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver {
-     override fun paywallViewDidFinishPurchase(
-         view: AdaptyUIPaywallView,
+ AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver {
+     override fun flowViewDidFinishPurchase(
+         view: AdaptyUIFlowView,
          product: AdaptyPaywallProduct,
          purchaseResult: AdaptyPurchaseResult
      ) {
          // custom logic after purchase
      }
  })

One callback is also renamed: paywallViewDidFailRendering becomes flowViewDidReceiveError. It fires for the same rendering errors as before, plus other non-purchase runtime errors:

- override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) {}
+ override fun flowViewDidReceiveError(view: AdaptyUIFlowView, error: AdaptyError) {}

See Handle flow & paywall events for the full list of callbacks.

Compose platform view

If you embed views with the Compose Multiplatform composable, AdaptyUIPaywallPlatformView(paywall, ...) is renamed to AdaptyUIFlowPlatformView(flow, ...). The event callbacks keep their onDid... names, except onDidFailRendering, which becomes onDidReceiveError:

- AdaptyUIPaywallPlatformView(
-     paywall = paywall,
+ AdaptyUIFlowPlatformView(
+     flow = flow,
      onDidFinishPurchase = { view, product, result -> /* ... */ },
  )

As in v3, the callbacks you pass here (and any observer registered via registerFlowEventsListener) run in addition to the global observer, not instead of it — your callback observes an event; it does not replace the global default. Keep the changed defaults in mind: for example, the global default no longer dismisses the view after a purchase.

New APIs

  • AdaptyUI.setObserverModeResolver(...) with an AdaptyUIObserverModeResolver — 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 an AdaptyUISystemRequestsHandler — reserved for system requests from a flow (OS permission prompts and app review requests). Flows don’t trigger these requests yet, so you don’t need to register a handler.
  • The new optional flowViewDidReceiveAnalyticEvent callback reports analytics events from a flow, starting with a screen view for every screen a user opens. See Track flow screen views.
  • AdaptyUI.openWebUrl(url, openIn) and AdaptyUI.requestAppReview() — these back the default OpenUrlAction handling and the default handleAppReviewRequest, so URLs and app-review prompts are handled natively out of the box. Call them directly only if you override those defaults.
  • AdaptyUIFlowView.locale — reports the localization the view was built with, so you can tell which one the user actually sees.
  • AdaptyConfig.ServerCluster.CN — a new server cluster option alongside DEFAULT and EU, for connecting your app to Adapty’s China servers.

Renamed external attribution APIs

Starting from SDK version 4.1, the APIs that pass attribution data from an external provider (Adjust, AppsFlyer, Branch, Tenjin, Apple Ads, or a custom one) are renamed to match the native SDKs, and the provider changes from a string to a type. There are no deprecated aliases, so existing call sites stop compiling until you update them.

updateAttribution → updateExternalAttribution

The method is renamed, its source parameter becomes provider, and that parameter now takes an AdaptyExternalAttributionProvider instead of a String:

- Adapty.updateAttribution(attribution, "adjust")
+ Adapty.updateExternalAttribution(attribution, AdaptyExternalAttributionProvider.ADJUST)

Attribution data is still a Map<String, Any>.

The call returns once the backend accepts the data for asynchronous processing. A successful result doesn’t mean the data has already been applied to the profile.

AdaptyExternalAttributionProvider

The provider is now a type with predefined values: APPLE_ADS, ADJUST, APPSFLYER, BRANCH, TENJIN, and CUSTOM. For any other provider, construct one from its identifier:

AdaptyExternalAttributionProvider("your_provider")

Constructing one directly also covers providers Adapty adds after this SDK release — the identifier reaches the backend unchanged rather than collapsing into an unknown value. Surrounding whitespace is trimmed.

Each predefined value wraps the same identifier you passed to updateAttribution before: AdaptyExternalAttributionProvider.APPLE_ADS.value is apple_search_ads, and the rest are their own lowercase names.

AdaptyProfile.appliedAttributionSources → appliedExternalAttributionProviders

The profile property that lists the attribution providers applied to the profile is renamed, and its element type changes accordingly:

- if (profile.appliedAttributionSources.contains("apple_search_ads")) {
+ if (profile.appliedExternalAttributionProviders.contains(AdaptyExternalAttributionProvider.APPLE_ADS)) {
      // Apple Ads attribution has been applied
  }

AdaptyPaywallProductSubscription → AdaptyProductSubscription

The subscription details type is renamed, because promoted products now carry the same type. Only the name changes — every property keeps its name and type:

- val subscription: AdaptyPaywallProductSubscription? = product.subscription
+ val subscription: AdaptyProductSubscription? = product.subscription

App Store promoted in-app purchases

SDK 4.1 delivers in-app purchases promoted on your App Store product page to your app on iOS. Earlier versions completed such a purchase automatically and gave your app no way to intercept it. From 4.1 the purchase waits for your code instead, so this needs action even if you never touched promoted purchases.

To support promoted purchases, register an OnPromotedPurchaseListener and complete the purchase by passing the product to Adapty.makePromotedPurchase. Without a registered listener, the purchase is held rather than completed: the user taps Buy on your App Store page and nothing happens in your app. Register the listener as early as you can — see In-app purchases from the App Store for the timing and the full example.

Choosing a specific layout

createFlowView, createNativeFlowView, and AdaptyUIFlowPlatformView take a new optional customLayoutId parameter. Pass it to render a specific layout in the flow’s layout configuration instead of the one the SDK picks automatically from the device type and screen size. The Flow & Paywall Builder doesn’t assign custom layout IDs yet, so leave this unset:

AdaptyUI.createFlowView(flow, customLayoutId = "tablet_landscape")

If no layout matches the ID, the flow loads without a view configuration. The parameter is optional and defaults to null, so existing calls are unaffected.

Default behavior changes

These changes do not cause compile errors, so test them at runtime:

  • Purchase completion: In v3, the default paywallViewDidFinishPurchase dismissed the view after any purchase result other than AdaptyPurchaseResult.UserCanceled. In v4, the default flowViewDidFinishPurchase is a no-op, so a flow stays open after a purchase until you dismiss it — matching iOS. If you relied on that auto-close, call view.dismiss() yourself once the purchase finishes.
  • Android system back: In v3, the default paywallViewDidPerformAction dismissed the view on both CloseAction and AndroidSystemBackAction. In v4, the default handles only CloseActionthe system back button no longer closes a flow on its own, matching iOS, where a flow can’t be dismissed by a system gesture. Give users an explicit way out (a Close button or an on_device_back action), or dismiss the view yourself in flowViewDidPerformAction.
  • View errors: In v3, the default paywallViewDidFailRendering did nothing. In v4, the default flowViewDidReceiveError dismisses the view — override it if you want to keep the view open or handle the error differently.
  • Views are single-use: After dismiss(), the view is destroyed. Call createFlowView again to present the flow once more.

Onboarding API deprecation

The legacy onboarding API is deprecated in v4 in favor of the Flow & Paywall Builder. It still works, but will be removed in a future release, so plan migration of your onboardings to the Flow & Paywall Builder.

Deprecated symbols: getOnboarding, getOnboardingForDefaultAudience, AdaptyUI.createOnboardingView, AdaptyUI.createNativeOnboardingView, and AdaptyUIOnboardingsEventsObserver.