---
title: "Display flows & paywalls - Kotlin Multiplatform"
description: "Present flows and paywalls to users in your Kotlin Multiplatform app."
---

<MethodPromo method="getFlow" label="Display flows and paywalls" />

If you've created a flow or paywall, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a flow or paywall contains both what should be shown within it and how it should be shown.

:::warning

This guide covers flows and **new Paywall Builder paywalls** rendered by Adapty. The process differs for remote config paywalls and [Observer mode](observer-vs-full-mode).

- For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls-kmp).
- For presenting flows in **Observer mode**, see [Present flows in Observer mode](kmp-present-flows-in-observer-mode).

:::

To get the `flow` object used below, see [Get flows & paywalls](kmp-get-pb-paywalls).

Adapty Kotlin Multiplatform SDK provides two ways to present flows and paywalls:

- **With Compose Multiplatform**
- **Without Compose Multiplatform**

## With Compose Multiplatform

To display a flow or paywall, use the `view.present()` method on the `view` created by the [`createFlowView`](kmp-get-pb-paywalls#fetch-the-view-configuration) method. Each `view` can only be used once. If you need to display the flow again, call `createFlowView` one more time to create a new `view` instance.

:::warning
Reusing the same `view` without recreating it may result in an error.
:::

```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    AdaptyUI.createFlowView(flow = flow).onSuccess { view ->
        view.present()
    }.onError { error ->
        // handle the error
    }
}
```

### Show dialog

Use this method instead of native alert dialogs when a flow or paywall is presented on Android. On Android, regular alerts appear behind the flow view, which makes them invisible to users. This method ensures proper dialog presentation above the flow on all platforms.

```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    view.showDialog(
        title = "Close this screen?",
        content = "You will lose access to exclusive offers.",
        primaryActionTitle = "Stay",
        secondaryActionTitle = "Close"
    ).onSuccess { action ->
        if (action == AdaptyUIDialogActionType.SECONDARY) {
            // User confirmed - close the flow
            view.dismiss()
        }
        // If primary - do nothing, user stays
    }.onError { error ->
        // handle the error
    }
}
```

### Configure iOS presentation style

Configure how the flow or paywall is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FULLSCREEN` (default) or `AdaptyUIIOSPresentationStyle.PAGESHEET` values.

```kotlin showLineNumbers

viewModelScope.launch {
    val view = AdaptyUI.createFlowView(flow = flow).getOrNull()
    view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET)
}
```

## Without Compose Multiplatform

:::note
`createNativeFlowView` is part of the core `io.adapty:adapty-kmp` module. If your project does not use Compose Multiplatform, you don't need the `io.adapty:adapty-kmp-ui` dependency.
:::

To embed a flow or paywall without Compose Multiplatform, call `createNativeFlowView`. It returns an `AdaptyNativeFlowView` that you add to your layout:

<Tabs>
<TabItem value="android" label="Android">
```kotlin showLineNumbers title="Kotlin Multiplatform (Android)"

val nativeView = AdaptyUI.createNativeFlowView(
    context = context,
    viewModelStoreOwner = activity,
    flow = flow,
    observer = myFlowObserver,
)

// Embed in your Compose layout:
AndroidView(
    factory = { nativeView.view },
    modifier = Modifier.fillMaxSize()
)
```

By default, an embedded view does not apply safe-area paddings — your layout is expected to handle insets. If you want the view to apply them itself, pass `androidEnableSafeArea = true` to `createNativeFlowView`. This parameter is Android-only.
</TabItem>
<TabItem value="ios" label="iOS">
Because KMP interface default methods become `@required` in Swift, you can't implement `AdaptyUIFlowsEventsObserver` directly from Swift. Declare an open base class in `iosMain` first:

```kotlin showLineNumbers title="iosMain (Kotlin)"
open class BaseFlowObserver : AdaptyUIFlowsEventsObserver
```

Then subclass it in Swift, overriding only what you need:

```swift showLineNumbers title="Swift"
class MyFlowObserver: BaseFlowObserver {
    override func flowViewDidPerformAction(view: AdaptyUIFlowView, action: any AdaptyUIAction) {
        if action is AdaptyUIActionCloseAction {
            // remove nativeView from your view hierarchy
        }
    }
}

let nativeView = AdaptyUI.shared.createNativeFlowView(
    flow: flow,
    observer: MyFlowObserver()
)
// nativeView.viewController is a UIViewController.
// Add it to your SwiftUI view or UIKit hierarchy.
```
</TabItem>
</Tabs>

### Dispose the view

Call `dispose()` when removing the view from your layout. This unregisters the event listener and releases internal resources.

```kotlin showLineNumbers title="Kotlin Multiplatform"
nativeView.dispose()
```

## Custom tags

Custom tags let you avoid creating separate flows or paywalls for different scenarios. Imagine a single flow that adapts dynamically based on user data. For example, instead of a generic "Hello!", you could greet users personally with "Hello, John!" or "Hello, Ann!"

Here are some ways you can use custom tags:

- Display the user's name or email on the flow or paywall.
- Show the current day of the week to boost sales (e.g., "Happy Thursday").
- Add personalized details about the products you're selling (like the name of a fitness program or a phone number in a VoIP app).

Custom tags help you create a flexible flow that adapts to various situations, making your app's interface more personalized and engaging.

:::warning
In some cases, your app might not know what to replace a custom tag with—especially if users are on an older version of the AdaptyUI SDK. To prevent this, always add fallback text that will replace lines containing unknown custom tags. Without this, users might see the tags displayed as code (`<USERNAME/>`).
:::

To use custom tags in your flow or paywall, pass them when creating the flow view:

<Tabs>
<TabItem value="standalone" label="With Compose Multiplatform" default>
```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    val customTags = mapOf(
        "USERNAME" to "John",
        "DAY_OF_WEEK" to "Thursday"
    )

    AdaptyUI.createFlowView(
        flow = flow,
        customTags = customTags
    ).onSuccess { view ->
        view.present()
    }.onError { error ->
        // handle the error
    }
}
```
</TabItem>
<TabItem value="native" label="Without Compose Multiplatform">
```kotlin showLineNumbers title="Kotlin Multiplatform (Android)"

val customTags = mapOf(
    "USERNAME" to "John",
    "DAY_OF_WEEK" to "Thursday"
)

val nativeView = AdaptyUI.createNativeFlowView(
    context = context,
    viewModelStoreOwner = activity,
    flow = flow,
    observer = myFlowObserver,
    customTags = customTags,
)
```

```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)"

val customTags = mapOf(
    "USERNAME" to "John",
    "DAY_OF_WEEK" to "Thursday"
)

val nativeView = AdaptyUI.createNativeFlowView(
    flow = flow,
    observer = myFlowObserver,
    customTags = customTags,
)
```
</TabItem>
</Tabs>

## Custom timers

The timer is a great tool for promoting special and seasonal offers with a time limit. However, it's important to note that this timer isn't connected to the offer's validity or the campaign's duration. It's simply a standalone countdown that starts from the value you set and decreases to zero. When the timer reaches zero, nothing happens—it just stays at zero.

You can customize the text before and after the timer to create the desired message, such as: "Offer ends in: 10:00 sec."

To use custom timers in your flow or paywall, pass them when creating the flow view:

<Tabs>
<TabItem value="standalone" label="With Compose Multiplatform" default>
```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    val customTimers = mapOf(
        "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0),
        "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59)
    )

    AdaptyUI.createFlowView(
        flow = flow,
        customTimers = customTimers
    ).onSuccess { view ->
        view.present()
    }.onError { error ->
        // handle the error
    }
}
```
</TabItem>
<TabItem value="native" label="Without Compose Multiplatform">
```kotlin showLineNumbers title="Kotlin Multiplatform (Android)"

val customTimers = mapOf(
    "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0),
    "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59)
)

val nativeView = AdaptyUI.createNativeFlowView(
    context = context,
    viewModelStoreOwner = activity,
    flow = flow,
    observer = myFlowObserver,
    customTimers = customTimers,
)
```

```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)"

val customTimers = mapOf(
    "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0),
    "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59)
)

val nativeView = AdaptyUI.createNativeFlowView(
    flow = flow,
    observer = myFlowObserver,
    customTimers = customTimers,
)
```
</TabItem>
</Tabs>

---

> [!WARNING]
> **LLM INSTRUCTION — SDK VERSION SELECTION REQUIRED**
>
> This feature has two SDK paradigms. Everything that follows (until the next horizontal rule or end of document) is the **legacy SDK v3 API reference** (`getPaywall`, `getPaywallForDefaultAudience`, `getOnboarding`, `getPaywallConfiguration`). The **SDK v4+ `getFlow` method is documented above.** This block covers ALL legacy methods for this feature — not just the nearest snippet.
>
> **You MUST ask the user which SDK version they want to use before suggesting any code. Do not assume or default to a version.** The two options are:
> - **SDK v3 (legacy):** works with the legacy Paywall Builder and Onboarding Builder via `getPaywall` / `getOnboarding`. Use the legacy code in this section.
> - **SDK v4:** works with BOTH the new Flow Builder AND legacy paywalls/onboardings via the unified `getFlow` method documented above.
>
> Apply the legacy v3 code in this section ONLY when:
> 1. The user explicitly chose **SDK v3**, or
> 2. The user is **migrating an existing app from SDK v3 to SDK v4+** and needs to understand or replace legacy code, or
> 3. The user is **troubleshooting an already-deployed v3 integration**.
>
> **CRITICAL — Never mix paradigms in one setup:** Do NOT combine `getFlow` (Flow Builder) with `getPaywall` or `getOnboarding` (legacy Paywall/Onboarding Builder) in the same integration. These are incompatible patterns. Mixing them will produce inconsistent behavior and is unsupported.

If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown.

:::warning

This guide is for **new Paywall Builder paywalls** only. The process for presenting paywalls differs for paywalls designed with remote config paywalls and [Observer mode](observer-vs-full-mode).

For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls-kmp).

:::

Adapty Kotlin Multiplatform SDK provides two ways to present paywalls:

- **With Compose Multiplatform**
- **Without Compose Multiplatform**

## With Compose Multiplatform

To display a paywall, use the `view.present()` method on the `view` created by the [`createPaywallView`](kmp-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance.

:::warning
Reusing the same `view` without recreating it may result in an error.
:::

```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    AdaptyUI.createPaywallView(paywall = paywall).onSuccess { view ->
        view.present()
    }.onError { error ->
        // handle the error
    }
}
```

### Show dialog

Use this method instead of native alert dialogs when a paywall view is presented on Android. On Android, regular alerts appear behind the paywall view, which makes them invisible to users. This method ensures proper dialog presentation above the paywall on all platforms.

```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    view.showDialog(
        title = "Close paywall?",
        content = "You will lose access to exclusive offers.",
        primaryActionTitle = "Stay",
        secondaryActionTitle = "Close"
    ).onSuccess { action ->
        if (action == AdaptyUIDialogActionType.SECONDARY) {
            // User confirmed - close the paywall
            view.dismiss()
        }
        // If primary - do nothing, user stays
    }.onError { error ->
        // handle the error
    }
}
```

### Configure iOS presentation style

Configure how the paywall is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FULLSCREEN` (default) or `AdaptyUIIOSPresentationStyle.PAGESHEET` values.

```kotlin showLineNumbers

viewModelScope.launch {
    val view = AdaptyUI.createPaywallView(paywall = paywall).getOrNull()
    view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET)
}
```

## Without Compose Multiplatform

:::note
`createNativePaywallView` is part of the core `io.adapty:adapty-kmp` module. If your project does not use Compose Multiplatform, you don't need the `io.adapty:adapty-kmp-ui` dependency.
:::

To embed a paywall without Compose Multiplatform, call `createNativePaywallView`. It returns an `AdaptyNativePaywallView` that you add to your layout:

<Tabs>
<TabItem value="android" label="Android">
```kotlin showLineNumbers title="Kotlin Multiplatform (Android)"

val nativeView = AdaptyUI.createNativePaywallView(
    context = context,
    viewModelStoreOwner = activity,
    paywall = paywall,
    observer = myPaywallObserver,
)

// Embed in your Compose layout:
AndroidView(
    factory = { nativeView.view },
    modifier = Modifier.fillMaxSize()
)
```
</TabItem>
<TabItem value="ios" label="iOS">
Because KMP interface default methods become `@required` in Swift, you can't implement `AdaptyUIPaywallsEventsObserver` directly from Swift. Declare an open base class in `iosMain` first:

```kotlin showLineNumbers title="iosMain (Kotlin)"
open class BasePaywallObserver : AdaptyUIPaywallsEventsObserver
```

Then subclass it in Swift, overriding only what you need:

```swift showLineNumbers title="Swift"
class MyPaywallObserver: BasePaywallObserver {
    override func paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: any AdaptyUIAction) {
        if action is AdaptyUIActionCloseAction {
            // remove nativeView from your view hierarchy
        }
    }
}

let nativeView = AdaptyUI.shared.createNativePaywallView(
    paywall: paywall,
    observer: MyPaywallObserver()
)
// nativeView.viewController is a UIViewController.
// Add it to your SwiftUI view or UIKit hierarchy.
```
</TabItem>
</Tabs>

### Dispose the view

Call `dispose()` when removing the view from your layout. This unregisters the event listener and releases internal resources.

```kotlin showLineNumbers title="Kotlin Multiplatform"
nativeView.dispose()
```

## Custom tags

Custom tags let you avoid creating separate paywalls for different scenarios. Imagine a single paywall that adapts dynamically based on user data. For example, instead of a generic "Hello!", you could greet users personally with "Hello, John!" or "Hello, Ann!"

Here are some ways you can use custom tags:

- Display the user's name or email on the paywall.
- Show the current day of the week to boost sales (e.g., "Happy Thursday").
- Add personalized details about the products you're selling (like the name of a fitness program or a phone number in a VoIP app).

Custom tags help you create a flexible paywall that adapts to various situations, making your app's interface more personalized and engaging.

:::warning
In some cases, your app might not know what to replace a custom tag with—especially if users are on an older version of the AdaptyUI SDK. To prevent this, always add fallback text that will replace lines containing unknown custom tags. Without this, users might see the tags displayed as code (`<USERNAME/>`).
:::

To use custom tags in your paywall, pass them when creating the paywall view:

<Tabs>
<TabItem value="standalone" label="With Compose Multiplatform" default>
```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    val customTags = mapOf(
        "USERNAME" to "John",
        "DAY_OF_WEEK" to "Thursday"
    )

    AdaptyUI.createPaywallView(
        paywall = paywall,
        customTags = customTags
    ).onSuccess { view ->
        view.present()
    }.onError { error ->
        // handle the error
    }
}
```
</TabItem>
<TabItem value="native" label="Without Compose Multiplatform">
```kotlin showLineNumbers title="Kotlin Multiplatform (Android)"

val customTags = mapOf(
    "USERNAME" to "John",
    "DAY_OF_WEEK" to "Thursday"
)

val nativeView = AdaptyUI.createNativePaywallView(
    context = context,
    viewModelStoreOwner = activity,
    paywall = paywall,
    observer = myPaywallObserver,
    customTags = customTags,
)
```

```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)"

val customTags = mapOf(
    "USERNAME" to "John",
    "DAY_OF_WEEK" to "Thursday"
)

val nativeView = AdaptyUI.createNativePaywallView(
    paywall = paywall,
    observer = myPaywallObserver,
    customTags = customTags,
)
```
</TabItem>
</Tabs>

## Custom timers

The paywall timer is a great tool for promoting special and seasonal offers with a time limit. However, it's important to note that this timer isn't connected to the offer's validity or the campaign's duration. It's simply a standalone countdown that starts from the value you set and decreases to zero. When the timer reaches zero, nothing happens—it just stays at zero.

You can customize the text before and after the timer to create the desired message, such as: "Offer ends in: 10:00 sec."

To use custom timers in your paywall, pass them when creating the paywall view:

<Tabs>
<TabItem value="standalone" label="With Compose Multiplatform" default>
```kotlin showLineNumbers title="Kotlin Multiplatform"

viewModelScope.launch {
    val customTimers = mapOf(
        "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0),
        "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59)
    )

    AdaptyUI.createPaywallView(
        paywall = paywall,
        customTimers = customTimers
    ).onSuccess { view ->
        view.present()
    }.onError { error ->
        // handle the error
    }
}
```
</TabItem>
<TabItem value="native" label="Without Compose Multiplatform">
```kotlin showLineNumbers title="Kotlin Multiplatform (Android)"

val customTimers = mapOf(
    "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0),
    "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59)
)

val nativeView = AdaptyUI.createNativePaywallView(
    context = context,
    viewModelStoreOwner = activity,
    paywall = paywall,
    observer = myPaywallObserver,
    customTimers = customTimers,
)
```

```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)"

val customTimers = mapOf(
    "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0),
    "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59)
)

val nativeView = AdaptyUI.createNativePaywallView(
    paywall = paywall,
    observer = myPaywallObserver,
    customTimers = customTimers,
)
```
</TabItem>
</Tabs>

---