# In-App Experiences Configure and control In-App Experiences in Flutter applications. # In-App Experiences > Pause, resume, and control display timing for In-App Experiences. In-App Experiences are automatically enabled when you integrate the Airship SDK. Use these methods to control when and how they are displayed. ## Pausing and Resuming Display You can pause and resume In-App Experiences to control when they are displayed to users. ```dart // Pause in-app experiences await Airship.inApp.setPaused(true); // Resume in-app experiences await Airship.inApp.setPaused(false); // Check if paused bool isPaused = await Airship.inApp.isPaused(); ``` ### Auto-Pause on Launch You can configure the SDK to automatically pause In-App Experiences on launch. This is useful if you want to defer showing In-App Experiences until after onboarding or other critical app flows. ```dart var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, autoPauseInAppAutomationOnLaunch: true ); Airship.takeOff(config); ``` See the [Flutter Setup guide](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/) for complete `takeOff` configuration options. When you're ready to display In-App Experiences, call `setPaused(false)`: ```dart await Airship.inApp.setPaused(false); ``` ## Display Interval Control the minimum time between In-App Experience displays to avoid overwhelming users. ```dart // Set display interval to 5 seconds (5000 milliseconds) await Airship.inApp.setDisplayInterval(5000); // Get current display interval int interval = await Airship.inApp.getDisplayInterval(); ``` The display interval is the minimum time (in milliseconds) that must pass between displaying In-App Experiences. # Embedded Content > Integrate Embedded Content into your Flutter app to display Scene content directly within your app's screens. For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see [Embedded Content]({{< ref "/guides/features/messaging/scenes/embedded-content.md" >}}). ## Adding an embedded view The `AirshipEmbeddedView` widget defines a place for Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `embeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```dart import 'package:airship_flutter/airship_flutter.dart'; // Show any "home_banner" Embedded Content AirshipEmbeddedView(embeddedId: "home_banner") ``` ## Sizing The `AirshipEmbeddedView` accepts optional `parentWidth` and `parentHeight` parameters for controlling its dimensions. If not provided, the widget uses the available width and height. Use `parentHeight` for a constant height instead of a height-constrained container, as this allows the view to properly collapse to zero height when content is dismissed. **Custom sizing** ```dart AirshipEmbeddedView( embeddedId: "home_banner", parentWidth: 400, parentHeight: 200, ) ``` ## Checking if embedded content is available Use `Airship.inApp.isEmbeddedAvailable` to check if embedded content is currently available for a given ID: ```dart import 'package:airship_flutter/airship_flutter.dart'; final isAvailable = Airship.inApp.isEmbeddedAvailable(embeddedId: "home_banner"); ``` ## Listening for embedded content updates Use `Airship.inApp.isEmbeddedAvailableStream` to observe changes in the availability of embedded content for a given ID. The stream emits a boolean indicating whether content is available to display. It immediately emits the current state upon subscription, then emits updates whenever the state changes. **Availability stream** ```dart import 'package:airship_flutter/airship_flutter.dart'; final subscription = Airship.inApp .isEmbeddedAvailableStream(embeddedId: "home_banner") .listen((isAvailable) { print("home_banner is available: $isAvailable"); }); // Cancel the subscription when no longer needed subscription.cancel(); ``` ## Listing all pending embedded content Use `Airship.inApp.getEmbeddedInfos()` to get a list of all `EmbeddedInfo` objects across all embedded IDs that are pending display. Pending content has been triggered and prepared but has not yet been displayed in an `AirshipEmbeddedView`. Each `EmbeddedInfo` contains the `embeddedId` it is associated with. ```dart List allPending = Airship.inApp.getEmbeddedInfos(); ``` To observe changes to pending embedded content, use the `onEmbeddedInfoUpdated` stream: ```dart Airship.inApp.onEmbeddedInfoUpdated.listen((List infos) { print("Pending embedded infos updated: $infos"); }); ``` ## Showing a placeholder when content is unavailable The `AirshipEmbeddedView` collapses to zero height when no content is available. Use `isEmbeddedAvailableStream` to toggle between a placeholder and the embedded view based on content availability. **Embedded view with placeholder** ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class HomeBanner extends StatelessWidget { const HomeBanner({super.key}); @override Widget build(BuildContext context) { return StreamBuilder( // The stream emits the current state immediately upon subscription stream: Airship.inApp.isEmbeddedAvailableStream(embeddedId: "home_banner"), builder: (context, snapshot) { final isAvailable = snapshot.data ?? false; if (!isAvailable) { // Display a placeholder when no content is available return const SizedBox( height: 200, child: Center(child: Text("No content available")), ); } // Display the Airship content when it becomes available return const AirshipEmbeddedView( embeddedId: "home_banner", parentHeight: 200, ); }, ); } } ``` # Custom Views > Register custom native views to use within Scenes. A *Custom View* is a native view from your mobile or web application embedded into a Scene. Custom Views can display any native content your app exposes, so you can reuse that existing content within any screen in a Scene. Custom Views allow you to embed native iOS and Android views within Scenes, giving you full control over design and layout while leveraging Airship's targeting and orchestration capabilities. ## Requirements To use Custom Views in Flutter, you must add native iOS and Android code to your Flutter project. Custom Views work by embedding Flutter widgets within native Airship custom view containers. ## Implementation Custom Views require native implementation on both iOS and Android platforms. For a complete working implementation, see this [Flutter Custom Views example](https://gist.github.com/rlepinski/e67f917114222e4529811e43f319a7ce). The basic pattern is: 1. **Set up Flutter routing** to handle custom view routes (e.g., `/custom/my-view`) 2. **Register custom views on iOS** using `AirshipCustomViewManager` 3. **Register custom views on Android** using `AirshipCustomViewManager` ### Flutter Routing Configure your app's routing to handle custom view paths: ```dart MaterialApp( onGenerateRoute: (settings) { // Pattern match on full route paths switch (settings.name) { case '/custom/my-banner': return MaterialPageRoute( builder: (context) => Material(child: MyBannerWidget()), ); case '/custom/my-product-card': return MaterialPageRoute( builder: (context) => Material(child: MyProductCard()), ); } // Handle other routes return null; }, home: MyHomePage(), ) ``` ### iOS Create `AirshipPluginExtender.swift` in your `ios/Runner/` directory: ```swift import Foundation import AirshipFrameworkProxy import ActivityKit import Flutter #if canImport(AirshipCore) import AirshipCore import AirshipAutomation #else import AirshipKit #endif @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { @MainActor public static func onAirshipReady() { AirshipCustomViewManager.shared.register(name: "example") { args in FlutterCustomViewWrapper(viewName: "example", properties: args.properties) } } } ``` Create `FlutterCustomView.swift` in your `ios/Runner/` directory: ```swift import Flutter import UIKit import SwiftUI import AirshipFrameworkProxy #if canImport(AirshipCore) import AirshipCore import AirshipAutomation #else import AirshipKit #endif /// SwiftUI wrapper for Flutter custom view @available(iOS 16.0, *) public struct FlutterCustomView: View { let viewName: String let properties: AirshipJSON? public var body: some View { FlutterCustomViewRepresentable(viewName: viewName, properties: properties) } } /// UIViewRepresentable bridge for SwiftUI @available(iOS 16.0, *) struct FlutterCustomViewRepresentable: UIViewRepresentable { let viewName: String let properties: AirshipJSON? func makeUIView(context: Context) -> FlutterCustomViewContainer { return FlutterCustomViewContainer(viewName: viewName, properties: properties) } func updateUIView(_ uiView: FlutterCustomViewContainer, context: Context) { // No updates needed } } /// Flutter custom view that embeds a Flutter widget public class FlutterCustomViewContainer: UIView { private let viewName: String private let properties: AirshipJSON? private var flutterEngine: FlutterEngine? private var flutterViewController: FlutterViewController? public init(viewName: String, properties: AirshipJSON?) { self.viewName = viewName self.properties = properties super.init(frame: .zero) setupView() } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupView() { backgroundColor = .systemGray6 clipsToBounds = true } override public func willMove(toWindow newWindow: UIWindow?) { super.willMove(toWindow: newWindow) if newWindow != nil { embedFlutterView() } else { removeFlutterView() } } override public func layoutSubviews() { super.layoutSubviews() flutterViewController?.view.frame = bounds } private func embedFlutterView() { flutterEngine = FlutterEngine(name: "airship_custom_\(viewName)") let result = flutterEngine?.run() guard result == true else { return } flutterViewController = FlutterViewController( engine: flutterEngine!, nibName: nil, bundle: nil ) guard let flutterViewController = flutterViewController else { return } addSubview(flutterViewController.view) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let self = self else { return } let route = "/custom/\(self.viewName)" self.flutterViewController?.pushRoute(route) } } private func removeFlutterView() { flutterViewController?.view.removeFromSuperview() flutterViewController = nil flutterEngine?.destroyContext() flutterEngine = nil } } ``` For more details on iOS custom views, see the [Apple Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/custom-views/). ### Android Create `AirshipExtender.kt` in your `android/app/src/main/kotlin/` directory: ```kotlin import android.content.Context import android.view.View import android.widget.FrameLayout import io.flutter.embedding.android.FlutterView import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor import com.urbanairship.android.layout.AirshipCustomViewManager import com.urbanairship.android.layout.AirshipCustomViewHandler import com.urbanairship.android.layout.AirshipCustomViewArguments import android.util.Log import io.flutter.embedding.android.FlutterTextureView import androidx.annotation.Keep import com.urbanairship.UAirship import com.urbanairship.android.framework.proxy.AirshipPluginExtender @Keep class AirshipExtender : AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { AirshipCustomViewManager.register("example", FlutterCustomViewHandler()) } } class FlutterCustomViewHandler : AirshipCustomViewHandler { override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View { return FlutterCustomView( context, args.name, args.properties ) } } class FlutterCustomView( context: Context, private val viewName: String, private val properties: com.urbanairship.json.JsonMap ) : FrameLayout(context) { private var flutterEngine: FlutterEngine? = null private var flutterView: FlutterView? = null private var isEngineInitialized = false companion object { private const val TAG = "FlutterCustomView" } init { setupView() } private fun setupView() { setBackgroundColor(android.graphics.Color.BLACK) if (layoutParams == null) { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) } } override fun onAttachedToWindow() { super.onAttachedToWindow() embedFlutterView() } private fun embedFlutterView() { if (isEngineInitialized) { return } try { val route = "/custom/$viewName" flutterEngine = FlutterEngine(context).apply { navigationChannel.setInitialRoute(route) dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) } val renderSurface = FlutterTextureView(context) flutterView = FlutterView(context, renderSurface) val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) addView(flutterView, params) flutterView?.attachToFlutterEngine(flutterEngine!!) flutterEngine?.lifecycleChannel?.appIsResumed() isEngineInitialized = true } catch (e: Exception) { Log.e(TAG, "Failed to create Flutter view", e) cleanup() } } override fun onDetachedFromWindow() { super.onDetachedFromWindow() cleanup() } override fun onWindowVisibilityChanged(visibility: Int) { super.onWindowVisibilityChanged(visibility) when (visibility) { View.VISIBLE -> { flutterEngine?.lifecycleChannel?.appIsResumed() } View.INVISIBLE, View.GONE -> { flutterEngine?.lifecycleChannel?.appIsPaused() } } } private fun cleanup() { try { flutterEngine?.lifecycleChannel?.appIsPaused() flutterView?.let { view -> view.detachFromFlutterEngine() removeView(view) } flutterView = null flutterEngine?.destroy() flutterEngine = null isEngineInitialized = false } catch (e: Exception) { Log.e(TAG, "Error during cleanup", e) } } } ``` For more details on Android custom views, see the [Android Custom Views documentation](https://www.airship.com/docs/developer/sdk-integration/android/in-app-experiences/custom-views/). ## Using Custom Views Once registered, Custom Views can be added to Scenes in the Airship dashboard: 1. Create or edit a Scene 2. Add the **Custom View** content element to a screen 3. Enter the view name (e.g., `my-custom-view`) that matches the name you registered in your native code 4. Optionally add key-value pairs to pass custom properties to the view The native view will be displayed within the Scene with the properties you configured.