# Flutter Integrate the Airship SDK into your Flutter applications for iOS and Android. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Message Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/flutter/message-center/getting-started/) and [Preference Center: Getting Started](https://www.airship.com/docs/developer/sdk-integration/flutter/preference-center/getting-started/). ```dart Airship.onDeepLink.listen((event) { var deepLink = event.deepLink; // Handle deep link }); ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Running Actions You can run actions programmatically using the `Airship.actions.run()` method. The method returns a `Future` that resolves with the action result. The action value can be a string, number, boolean, null, object, or array. **Running an action** ```dart // Run an action with a string value using async/await try { String? result = await Airship.actions.run("action_name", "action_value"); print("Action result: $result"); } catch (error) { print("Action error: $error"); } // Run an action with a Map value try { String? result = await Airship.actions.run("action_name", { "key": "value", "number": 42 }); print("Action result: $result"); } catch (error) { print("Action error: $error"); } // Run an action without a value try { String? result = await Airship.actions.run("action_name", null); print("Action result: $result"); } catch (error) { print("Action error: $error"); } // Run an action using Future.then() Airship.actions.run("action_name", "action_value") .then((result) { print("Action result: $result"); }) .catchError((error) { print("Action error: $error"); }); ``` ## Custom Actions You can register custom actions with an [Airship extender](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/extending-airship/) and with native SDK APIs. See the native platform documentation for details: - [iOS Actions](https://www.airship.com/docs/developer/sdk-integration/apple/actions/) - [Android Actions](https://www.airship.com/docs/developer/sdk-integration/android/actions/) # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. ```dart var flag = await Airship.featureFlagManager.flag("my-flag"); if (flag.isEligible) { // Do something with the flag } else { // Disable feature or use default behavior } ``` ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/privacy-manager/). ```dart Airship.featureFlagManager.trackInteraction(flag) ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. ```dart Airship.featureFlagManager.flag("another_rad_flag").then((flag) => { if (flag.isEligible) { // Do something with the flag } }).catchError((error) => { debugPrint("flag error: $error") }); ``` # Live Activities > Integrate Live Activities into your Flutter app to display real-time updates on the iOS Lock Screen and Dynamic Island. {{< badge "axp" >}} For the push API method, see the [iOS Live Activities](https://www.airship.com/docs/guides/messaging/features/ios-live-activities/) messaging guide. See also the [iOS Live Activities](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### App setup Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/#extending-airship), make a call to `LiveActivityManager.shared.setup` to configure any Live Activities for the app. Call `configurator.register` for each Live Activity type that your application defines and include a block on how to parse the name of the activity that you will use to track on Airship. This name will be used to send updates through APNS. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit // This class header is required to be automatically picked up by the Airship plugin: @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { if #available(iOS 16.1, *) { // Will throw if called more than once try? LiveActivityManager.shared.setup { configurator in // Call for each Live Activity type await configurator.register(forType: Activity.self) { attributes in // Track this property as the Airship name for updates attributes.gameID } } } // other setup } } ``` ### Starting Live Activities For any Live Activities configured, you can start a new one using the `start` method: ```dart if (Platform.isIOS) { LiveActivityStartRequest startRequest = LiveActivityStartRequest( attributesType: 'SportsActivityAttributes', attributes: { "gameID": "sports-game-123", }, content: LiveActivityContent(status: 'Game Pending', relevanceScore: 0.0)); await Airship.liveActivityManager.start(startRequest); } ``` ### Updating Live Activities To update, use `update`, but you will need the activity ID. ```dart if (Platform.isIOS) { List activities = await Airship.liveActivityManager.listAll(); LiveActivity? activity = activities .where((activity) => activity.attributes.gameID == 'sports-game-123') .firstOrNull; if (activity != null) { LiveActivityContent content = LiveActivityContent( state: {'status': 'Game starting!'}, relevanceScore: 0.0, ); LiveActivityUpdateRequest updateRequest = LiveActivityUpdateRequest( attributesType: 'SportsGameAttributes', activityId: activity.id, content: content, ); await Airship.liveActivityManager.update(updateRequest); } } ``` ### Ending Live Activities To end is similar to `update`. Use `end` with the activity ID: ```dart if (Platform.isIOS) { List activities = await Airship.liveActivityManager.listAll(); LiveActivity? activity = activities .where((activity) => activity.attributes.gameID == 'sports-game-123') .firstOrNull; if (activity != null) { LiveActivityStopRequest stopRequest = LiveActivityStopRequest( attributesType: 'SportsGameAttributes', activityId: activity.id, dismissalPolicy: LiveActivityDismissalPolicyDefault(), ); await Airship.liveActivityManager.end(stopRequest); } } ``` # Live Updates > Integrate Live Updates into your Flutter app to update content in real-time without requiring an app update. {{< badge "axp" >}} For the push API method, see the [Android Live Updates](https://www.airship.com/docs/guides/messaging/features/android-live-updates/) messaging guide. See also the [Android Live Updates](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ### Creating a handler The Airship SDK supports two types of Live Update handlers: * `NotificationLiveUpdateHandler` — Displays a notification with a custom layout, with content updated by the Live Update. * `CustomLiveUpdateHandler` — Receives Live Update events and provides flexibility to display content using a custom implementation. This can be used to power home screen widgets, views embedded in the app, and more. Each handler type has two different interfaces that may be implemented, to support suspending or callback-based code: * `SuspendLiveUpdateNotificationHandler` * `CallbackLiveUpdateNotificationHandler` * `SuspendLiveUpdateCustomHandler` * `CallbackLiveUpdateCustomHandler` The following `SampleLiveUpdateHandler` reads content from the Live Update payload and displays scores for a sports game in a custom notification layout, using `RemoteViews`: ```kotlin class SampleLiveUpdateHandler : SuspendLiveUpdateNotificationHandler() { override suspend fun onUpdate( context: Context, event: LiveUpdateEvent, update: LiveUpdate ): LiveUpdateResult { // Read content_state fields from the Live Update payload val teamOneScore = update.content.opt("team_one_score").getInt(0).toString() val teamTwoScore = update.content.opt("team_two_score").getInt(0).toString() val statusUpdate = update.content.opt("status_update").optString() // Expanded notification layout val bigLayout = RemoteViews(context.packageName, R.layout.sports_big).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) setTextViewText(R.id.statusUpdate, statusUpdate) } // Collapsed notification layout val smallLayout = RemoteViews(context.packageName, R.layout.sports_small).apply { setTextViewText(R.id.teamOneScore, teamOneScore) setTextViewText(R.id.teamTwoScore, teamTwoScore) } // Create the notification builder val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_EVENT) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(smallLayout) .setCustomBigContentView(bigLayout) // Return 'ok' with the notification builder. // The Airship SDK will handle posting the notification. // Returning LiveUpdateResult.cancel() will end the Live Update and dismiss the notification. return LiveUpdateResult.ok(builder) } companion object { private const val NOTIFICATION_CHANNEL_ID = "sports" } } ``` ### Registering a handler Handlers must be registered with `LiveUpdateManager` in order to receive Live Update events. This should be done *once* after `takeOff`. Using the [AirshipPluginExtender](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/#extending-airship), register the types. ```kotlin @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { LiveUpdateManager.shared().run { register(type = "notification", handler = SampleLiveUpdateHandler()) } } } ``` > **Note:** The `type` used above, `"notification"`, is used to map Live Update events to the corresponding handler in your app. > The value can be any string that is unique across all handlers registered by an app. This also allows a single handler to manage multiple Live Updates that each have a unique `name`. ### Starting Live Updates Live Updates can be started from within the app. ```dart if (Platform.isAndroid) { LiveUpdateStartRequest createRequest = LiveUpdateStartRequest( name: "sports-game-123", type: 'notification', content: { 'team_one_score': 0, 'team_two_score': 0, 'status_update': 'Game started!' } ); await Airship.liveUpdateManager.start(createRequest); } ``` ### Updating Live Updates Live Updates can be updated from within the app. ```dart if (Platform.isAndroid) { List updates = await Airship.liveUpdateManager.listAll(); LiveUpdateUpdateRequest request = LiveUpdateUpdateRequest( name: "sports-game-123", content: { 'team_one_score': 0, 'team_two_score': 0, 'status_update': 'Game started!' } ); await Airship.liveUpdateManager.update(request); } ``` ### Ending Live Updates You can end a Live Update from within the app. ```dart if (Platform.isAndroid) { List updates = await Airship.liveUpdateManager.listAll(); LiveUpdateEndRequest stopRequest = LiveUpdateEndRequest( name: "sports-game-123" ); await Airship.liveUpdateManager.end(stopRequest); } ``` ### Clearing all active Live Updates During development, it can be useful to reset Live Update tracking on app launch. This allows any Live Updates to be started fresh, even if they were already started during a previous launch. To end all currently active Live Updates, call the `clearAll()` method on `LiveUpdateManager`. ```dart if (Platform.isAndroid) { await Airship.liveUpdateManager.clearAll(); } ``` # Flutter Plugin Changelog > The latest updates to the Airship Flutter plugin. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 12.0.0 May 12, 2026 Major release that raises the minimum supported Flutter version to 3.24.0 to fully support Swift Package Manager on iOS. ### Changes - Raised minimum Flutter version to 3.24.0 and Dart SDK to 3.5.0 - Improved Swift Package Manager support for the iOS plugin ## 11.4.0 May 2, 2026 Minor release that updates the Android SDK to 20.7.0 and the iOS SDK to 20.7.0. ### Changes - Updated Android SDK to [20.7.0](https://github.com/urbanairship/android-library/releases/tag/20.7.0) - Updated iOS SDK to [20.7.0](https://github.com/urbanairship/ios-library/releases/tag/20.7.0) ## 11.3.1 April 9, 2026 Patch release that fixes iOS cold start push notification and deep link events not firing. ### Changes - Fixed iOS cold start push notification and deep link events not firing due to the plugin loader initializing too late to set `UNUserNotificationCenter.delegate` ## 11.3.0 April 1, 2026 Minor release that updates the Android SDK to 20.6.1 and the iOS SDK to 20.6.0 ### Changes - Updated Android SDK to [20.6.1](https://github.com/urbanairship/android-library/releases/tag/20.6.1) - Updated iOS SDK to [20.6.0](https://github.com/urbanairship/ios-library/releases/tag/20.6.0) ## 11.2.0 March 19, 2026 Minor release that updates the Android SDK to 20.5.0 and the iOS SDK to 20.5.0. ### Changes - Updated Android SDK to [20.5.0](https://github.com/urbanairship/android-library/releases/tag/20.5.0) - Updated iOS SDK to [20.5.0](https://github.com/urbanairship/ios-library/releases/tag/20.5.0) ## 11.1.0 January 23, 2026 Minor release that includes accessibility improvements for Message Center and fixes a potential crash on Android. ### Changes - Updated Android SDK to [20.1.1](https://github.com/urbanairship/android-library/releases/tag/20.1.1) - Updated iOS SDK to [20.1.1](https://github.com/urbanairship/ios-library/releases/tag/20.1.1) - Fixed a potential crash in Android Scenes with specific image and display settings. - Improved VoiceOver focus handling for Message Center on iOS. - Fixed an issue where the Message Center title was not being marked as a heading on Android. ## 11.0.0 January 14, 2026 Major release that updates the Android SDK to 20.0.4 and iOS SDK to 20.0.2. ### Changes - Updated Android SDK to [20.0.6](https://github.com/urbanairship/android-library/releases/tag/20.0.6) - Updated iOS SDK to [20.0.3](https://github.com/urbanairship/ios-library/releases/tag/20.0.3) - Updated Kotlin to 2.0.21 - Minimum iOS deployment target is now iOS 16.0 (requires Xcode 14+) - The `AirshipPluginExtender.onAirshipReady` method no longer receives a `UAirship` instance. Use the static `Airship` accessor instead. ## 10.10.1 November 15, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Updated Android SDK to [19.13.6](https://github.com/urbanairship/android-library/releases/tag/19.13.6) - Updated iOS SDK to [19.11.2](https://github.com/urbanairship/ios-library/releases/tag/19.11.2) ## 10.10.0 November 3, 2025 Minor release that updates the Android SDK to 19.13.5 and the iOS SDK to 19.11.1 ### Changes - Updated Android SDK to [19.13.5](https://github.com/urbanairship/android-library/releases/tag/19.13.5) - Updated iOS SDK to [19.11.1](https://github.com/urbanairship/ios-library/releases/tag/19.11.1) ## 10.9.0 October 29, 2025 Minor release that updates the Android SDK to 19.13.4 and the iOS SDK to 19.11.0 ### Changes - Updated Android SDK to [19.13.4](https://github.com/urbanairship/android-library/releases/tag/19.13.4) - Updated iOS SDK to [19.11.0](https://github.com/urbanairship/ios-library/releases/tag/19.11.0) - Fixed an issue where the app would hang when offline due to improper exception handling in feature flags. ## 10.8.0 August 27, 2025 Minor release that updates the Android SDK to 19.11.0 and the iOS SDK to 19.8.3. ### - Updated Android SDK to [19.11.0](https://github.com/urbanairship/android-library/releases/tag/19.11.0) - Updated iOS SDK to [19.8.3](https://github.com/urbanairship/ios-library/releases/tag/19.8.3) - Updated Android event emitting to wait for an attached activity for all events but background push recieved. - Fixed Feature Flag method bindings for `Airship.featureFlagManager.trackInteraction` and `Airship.featureFlagManager.setFlagInResultCache` on Android. ## 10.7.1 August 21, 2025 Patch release with several bug fixes for Scenes, including an important reporting fix for embedded content. ### Changes - Updated Android SDK to [19.10.2](https://github.com/urbanairship/android-library/releases/tag/19.10.2) - Updated iOS SDK to [19.8.2](https://github.com/urbanairship/ios-library/releases/tag/19.8.2) ## 10.7.0 August 1, 2025 Minor release that updates the Android SDK to 19.10.0 and the iOS SDK to 19.8.0 ### Changes - Updated Android SDK to [19.10.0](https://github.com/urbanairship/android-library/releases/tag/19.10.0) - Updated iOS SDK to [19.8.0](https://github.com/urbanairship/ios-library/releases/tag/19.8.0) ## 10.6.0 July 24, 2025 Minor release that fixes Flutter methods calls to avoid crashes when the native side throws an error. Apps using Feature flags are encouraged to update. ### Changes - Fixed crash for Feature Flags methods ## 10.5.0 June 26, 2025 Minor release that updates the Android SDK to 19.9.1 and the iOS SDK to 19.6.1 ### Changes - Updated Android SDK to [19.9.1](https://github.com/urbanairship/android-library/releases/tag/19.9.1) - Updated iOS SDK to [19.6.1](https://github.com/urbanairship/ios-library/releases/tag/19.6.1) - Added support for Android `logPrivacyLevel` configuration option in `AndroidConfig` ## 10.4.0 May 16, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. - Updated Android SDK to [19.7.0](https://github.com/urbanairship/android-library/releases/tag/19.7.0) - Updated iOS SDK to [19.4.0](https://github.com/urbanairship/ios-library/releases/tag/19.4.0) ## 10.3.1 May 9, 2025 Patch release that updates the iOS SDK to 19.3.2 ### Changes - Updated iOS SDK to [19.3.2](https://github.com/urbanairship/ios-library/releases/tag/19.3.2) ## 10.3.0 May 5, 2025 Minor release that updates the Android SDK to 19.6.2 and the iOS SDK to 19.3.1 and fixes an Embedded View bug. ### Changes - Updated Android SDK to [19.6.2](https://github.com/urbanairship/android-library/releases/tag/19.6.2) - Updated iOS SDK to [19.3.1](https://github.com/urbanairship/ios-library/releases/tag/19.3.1) - Added support for JSON attributes - Added new method `Airship.channel.waitForChannelId()` that waits for the channel ID to be created - Fixed bug in `Airship.inApp.isEmbeddedAvailableStream` that disrupted gated rendering of Embedded Views ## 10.2.0 March 27, 2025 Minor release that updates the Android SDK to 19.4.0 and the iOS SDK to 19.1.1 ### Changes - Updated Android SDK to [19.1.1](https://github.com/urbanairship/android-library/releases/tag/19.1.1) - Updated iOS SDK to [19.4.0](https://github.com/urbanairship/ios-library/releases/tag/19.4.0) ## 10.1.0 February 12, 2025 Minor release that updates the Android SDK to 19.1.0 and the iOS SDK to 19.0.3 ### Changes - Updated Android SDK to [19.1.0](https://github.com/urbanairship/android-library/releases/tag/19.1.0) - Updated iOS SDK to [19.0.3](https://github.com/urbanairship/ios-library/releases/tag/19.0.3) ## 10.0.0 February 6, 2025 Major release that updates the Android SDK to 19.0.0 and the iOS SDK to 19.0.3 ### Changes - Updated Android SDK to [19.0.0](https://github.com/urbanairship/android-library/releases/tag/19.0.0) - Updated iOS SDK to [19.0.3](https://github.com/urbanairship/ios-library/releases/tag/19.0.3) ## 9.1.1 January 17, 2025 Patch release that updates the Android SDK to 18.6.0 and the iOS SDK to 18.14.2 ### Changes - Updated Android SDK to [18.6.0](https://github.com/urbanairship/android-library/releases/tag/18.6.0) - Updated iOS SDK to [18.14.2](https://github.com/urbanairship/ios-library/releases/tag/18.14.2) ## 9.1.0 December 7, 2024 Minor release that updates the Android Airship SDK to 18.5.0 and iOS Airship SDK to 18.13.0 ### Changes - Updated Android SDK to [18.5.0](https://github.com/urbanairship/android-library/releases/tag/18.5.0). - Updated iOS SDK to [18.13.0](https://github.com/urbanairship/ios-library/releases/tag/18.13.0). ## 9.0.1 November 28, 2024 Patch release that updates the iOS SDK to 18.12.2 and Android SDK to 18.4.2 ### Changes - Updated Android SDK to 18.4.2. - Updated iOS SDK to 18.12.2. ## 9.0.0 November 16, 2024 Major version release that drops support for v1 embeddings. ### Changes - Drops support for deprecated v1 embeddings. ## 8.0.4 November 8, 2024 Patch release that resolves an issue with Firebase integrations and fixes an issue with opt-in checks when requestAuthorizationToUseNotifications is set to false on iOS. ### Changes - Updated Airship iOS SDK to [18.12.1](https://github.com/urbanairship/ios-library/releases/tag/18.12.1) - Fixed issues caused by swizzling conflicts with some Firebase framework integrations. - Fixed opt-in check permissions querying when requestAuthorizationToUseNotifications is set to false on iOS. ## 8.0.3 November 7, 2024 Patch release that resolves an issue with Firebase integrations and fixes an issue with opt-in checks when requestAuthorizationToUseNotifications is set to false on iOS. ### Changes - Updated Airship iOS SDK to [18.12.1](https://github.com/urbanairship/ios-library/releases/tag/18.12.0) - Fixed issues caused by swizzling conflicts with some Firebase framework integrations. - Fixed opt-in check permissions querying when requestAuthorizationToUseNotifications is set to false on iOS. ## 8.0.2 November 4, 2024 ## Version 8.0.2 - November 4, 2024 Patch release that updates to latest SDKs and resolves an issue with Firebase integrations. Applications that integrate with Firebase are encouraged to update. ### Changes - Updated Airship Android SDK to [18.4.0](https://github.com/urbanairship/android-library/releases/tag/18.4.0) - Updated Airship iOS SDK to [18.12.0](https://github.com/urbanairship/ios-library/releases/tag/18.12.0) - Fixed token clearing during registration retries when Firebase is not yet ready. ## 8.0.1 October 25, 2024 Patch release that fixes an issue with event streams that causes deep links to fail when the app is launched from a terminated state. Apps that use deep linking are encouraged to update. ### Changes - Fixed event stream handling for initial events - Fixed tracking live activities started from a push notification ## 8.0.0 October 25, 2024 Major version that makes it easier to include Airship in a hybrid app. The only breaking change is when extending the AirshipPluginExtender protocol on java there is a new extendConfig(Contex, AirshipConfigOptions.Builder) method to implement. Most application will not be affected. ### Changes - Added new methods to the plugin extender to make hybrid app integrations easier ## 7.9.0 October 21, 2024 Minor version release with several new features including: iOS Live Activities, Android Live Updates, Message Center improvements, and iOS notification service extension support in the iOS example project. ### Changes - Updated Airship Android SDK to [18.3.3](https://github.com/urbanairship/android-library/releases/tag/18.3.3) - Updated Airship iOS SDK to [18.11.1](https://github.com/urbanairship/ios-library/releases/tag/18.11.1) - Added `notificationPermissionStatus` to `PushNotificationStatus` - Added options to `enableUserNotifications` to specify the `PromptPermissionFallback` when enabling user notifications - Added new `showMessageCenter(messageId?: string)` and `showMessageView(messageId: string)` to `MessageCenter` to display the OOTB UI even if `autoLaunchDefaultMessageCenter` is disabled - Added new APIs to manage [iOS Live Activities](https://docs.airship.com/platform/mobile/ios-live-activities/) - Added new APIs to manage [Android Live Updates](https://docs.airship.com/platform/mobile/android-live-updates/) - Added a new [iOS plugin extender]() to modify the native Airship SDK after takeOff - Added new [Android plugin extender]() to modify the native Airship SDK after takeOff ## 7.8.2 September 13, 2024 Patch release that fixes a potential Swift compile error. ### Changes - Fixed a potential Swift compile error. ## 7.4.0 September 10, 2024 Minor release that updates the Android SDK to 17.8.1 and iOS SDK to 18.2.2 ### Changes - Updated Android SDK to 17.8.1. - Updated iOS SDK to 18.2.2. ## 7.8.1 September 10, 2024 Patch release that brings back in-app messages methods and fixes a potential Swift compile error. ### Changes - Brought back `setPaused()`, `isPaused()`, `setDisplayInterval()` and `displayInterval()` methods. - Fixed a potential Swift compile error. ## 7.8.0 September 4, 2024 Minor release that adds early access support for Embedded Content. ## Changes - Adds AirshipEmbeddedView and listener methods to Airship.inApp for Embedded Content. ## 7.7.1 August 17, 2024 Patch release that adds a message center message list refresh operation on iOS. This allows message center messages to properly display when launched from a push while the iOS app is backgrounded. iOS apps that open message center messages directly from push notifications are encouraged to update. ### Changes - Refresh message center messages when message is initially unavailable on iOS. ## 7.7.0 August 13, 2024 Minor release that fixes test devices audience check, holdout group experiments displays and in-app experience displays when resuming from a paused state. Apps that use in-app experiences are encouraged to update. ### Changes - Updated Android SDK to 18.1.6. - Updated iOS SDK to 18.7.2. - Fixed test devices audience check. - Fixed holdout group experiments displays. - Fixed in-app experience displays when resuming from a paused state. ## 7.6.0 July 11, 2024 Minor release that updates the Android SDK to 18.1.1 and the iOS SDK to 18.5.0. ### Changes - Updated Android SDK to 18.1.1 - Updated iOS SDK to 18.5.0 - Updated airship-mobile-framework-proxy to 7.0.0 - Updated Kotlin version to 1.9.0 - Added support for configuring log privacy level on iOS ## 7.5.0 June 21, 2024 Minor release that updates iOS SDK to 18.4.1, updates Android compileSDKVersion from 33 to 34, sets Android source and target compatibility to Java 17, updates android example build configuration, improves example UI, and updates the airship mobile framework proxy to 6.3.1 which includes a fix for event management. ### Changes - Updated iOS SDK to 18.4.1 - Updated airship-mobile-framework-proxy to 6.3.1 - Fixed Event Emitter bug - Updated Android compileSDKVersion from 33 to 34 and set source and target compatibility to Java 17 - Updated Android example build configration - Improved example UI [View Older Releases](https://github.com/urbanairship/airship-flutter/releases?q=created%3A%3C2024-05-15&expanded=true) # Flutter Plugin Resources > API documentation, source code, and changelogs for the Airship Flutter plugin. ## Platform Support {#platform-support} | Feature | iOS | Android | |----------------------------------------|-----|---------| | Push Notifications | ✅ | ✅ | | Live Activities | ✅ | ❌ | | Live Updates | ❌ | ✅ | | In-App Experiences | ✅ | ✅ | | Custom Views | ✅ | ✅ | | Embedded Content | ✅ | ✅ | | Message Center | ✅ | ✅ | | Preference Center | ✅ | ✅ | | Feature Flags | ✅ | ✅ | | Analytics | ✅ | ✅ | | Contacts | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ## API References * [Flutter API Documentation](https://www.airship.com/docs/reference/libraries/flutter/latest/) ## GitHub Samples * [Flutter Sample App](https://github.com/urbanairship/airship-flutter/tree/main/example) ## Source * [Source](https://github.com/urbanairship/airship-flutter) ## Changelog * [Flutter Changelog](https://www.airship.com/docs/developer/sdk-integration/flutter/changelog/) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [Flutter License](https://github.com/urbanairship/airship-flutter/blob/main/LICENSE) ## SDK Installation Install and configure the Airship Flutter plugin for iOS and Android applications. # Install and Set Up the Flutter Plugin > Learn how to install the Airship Flutter plugin, configure iOS and Android platforms, and initialize the SDK. This guide walks you through installing the Airship Flutter plugin and configuring it for both iOS and Android platforms. ## Requirements - Flutter 3.0.2 or higher - iOS 15+ (Xcode 16+) - Android API 23+ ## Install the plugin 1. Add the Airship dependency to your package's `pubspec.yaml` file: ```yaml dependencies: airship_flutter: ^flutterPluginVersion ``` 2. Install your Flutter package dependencies by running the following in the command line at your project's root directory: `flutter pub get` 3. Import Airship into your Dart code: ```dart import 'package:airship_flutter/airship_flutter.dart'; ``` ## Initialize Airship The Airship SDK requires a single entry point called `takeOff` to initialize. Call `takeOff` in your app's `main()` function before running the app. This ensures Airship is ready before any widgets are built. ### Get your app credentials Before calling `takeOff`, you'll need your app credentials from the Airship dashboard: 1. Log in to the [Airship dashboard](https://go.airship.com/) and open your project. 1. Select the dropdown menu (▼) next to your project name, and then **Project details**. 1. Copy your **App Key** and **App Secret**. > **Important:** Airship provides separate credentials for development and production environments. The SDK automatically selects the correct credentials based on your build configuration. However, Flutter doesn't have a built-in way to detect this, so you can use the `inProduction` flag or configure based on build mode. ### Configure takeOff The following example shows how to configure and call `takeOff` in your Flutter app: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; void main() { // Ensure Flutter is initialized WidgetsFlutterBinding.ensureInitialized(); // Configure Airship var config = AirshipConfig( // Development environment credentials defaultEnvironment: ConfigEnvironment( appKey: "YOUR_DEVELOPMENT_APP_KEY", appSecret: "YOUR_DEVELOPMENT_APP_SECRET" ), // Production environment credentials (optional but recommended) productionEnvironment: ConfigEnvironment( appKey: "YOUR_PRODUCTION_APP_KEY", appSecret: "YOUR_PRODUCTION_APP_SECRET" ), // Set to true for production builds inProduction: false, // Set to true for production or use const bool.fromEnvironment('dart.vm.product') // Cloud site: Site.us for US, Site.eu for EU site: Site.us ); // Initialize Airship Airship.takeOff(config); // Run your app runApp(MyApp()); } ``` ### Configuration options Key configuration options include: * **defaultEnvironment**: Credentials for your development/staging environment * **productionEnvironment**: Credentials for your production environment (optional but recommended) * **inProduction**: Set to `true` for production builds, `false` for development. You can use `const bool.fromEnvironment('dart.vm.product')` to automatically detect release builds * **site**: Set to `Site.us` for US cloud projects or `Site.eu` for EU cloud projects For additional configuration options (such as URL allowlists), see [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/advanced-configuration/). > **Note:** Once `takeOff` is called, the config is stored and applied for future app sessions. If you call `takeOff` again with a different config, the new config will not take effect until the next app restart. ## Test the integration After completing the setup, verify your integration to ensure everything is working correctly. 1. **Build and run your app** on both iOS and Android (if applicable): `flutter run` 2. **Check the console logs** for Airship initialization: * Look for log messages indicating successful SDK initialization * You should see a **Channel ID** in the logs—this is the unique identifier for the device Example log output: ``` Airship takeOff succeeded Channel ID: 01234567-89ab-cdef-0123-456789abcdef ``` 3. **Verify in the Airship dashboard**: 1. Open your project. 1. Go to **Audience**, then **Contact Management**. 1. Search for the Channel ID from your logs. The channel should appear with the correct platform, iOS or Android. If you don't see a channel ID or encounter errors during initialization, see the [Troubleshooting](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/) guide for common problems and solutions. ## Next steps Now that you've successfully integrated the Airship SDK: 1. [**Enable push notifications**](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/getting-started/) and configure notification handling 2. [**Identify users**](https://www.airship.com/docs/developer/sdk-integration/flutter/audience/contacts/) with contacts and named users 3. [**Set up Message Center**](https://www.airship.com/docs/developer/sdk-integration/flutter/message-center/getting-started/) for persistent messaging 4. [**Configure analytics**](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/analytics/) to track user behavior If you don't see a channel ID or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/initialization/) for common problems and solutions. # Logging > Configure log levels and privacy settings to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. By default, the log level is set to **Info** for development builds and **Error** for production builds to ensure clean logs in a live environment. ## Log levels The following log levels are available, ordered from most to least verbose. | Log Level | Description | | :-------- | :---------- | | **Verbose** | Reports highly detailed SDK status, which is useful for deep debugging and troubleshooting. | | **Debug** | Reports general SDK status with more detailed information than `Info`. | | **Info** | Reports general SDK status and lifecycle events. | | **Warning** | Used for API deprecations, invalid setup, and other potentially problematic situations that are generally recoverable. | | **Error** | Used for critical errors, exceptions, and other situations that the SDK cannot gracefully handle. | | **None** | Disables all logging. | ## Configure log levels You can set the log level in the Airship config when calling `takeOff`. This setting acts as a minimum threshold—only logs at that level and higher will be output. ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, // Set log level for both platforms logLevel: LogLevel.verbose, // Optional: Set platform-specific log privacy levels iosConfig: IOSConfig( logPrivacyLevel: IOSLogPrivacyLevel.public ), androidConfig: AndroidConfig( logPrivacyLevel: AndroidLogPrivacyLevel.public ) ); Airship.takeOff(config); runApp(MyApp()); } ``` ### Recommended log levels by environment | Environment | Recommended Level | Purpose | | :---------- | :--------------- | :------ | | **Development** | `LogLevel.verbose` or `LogLevel.debug` | Maximum detail for debugging | | **Staging/QA** | `LogLevel.info` | General status information | | **Production** | `LogLevel.error` or `LogLevel.warning` | Only critical issues | ### Example: Conditional logging Set different log levels based on build mode: ```dart import 'package:flutter/foundation.dart'; var config = AirshipConfig( // ... other config ... // Use verbose logging in debug builds, errors only in release logLevel: kDebugMode ? LogLevel.verbose : LogLevel.error, iosConfig: IOSConfig( logPrivacyLevel: kDebugMode ? IOSLogPrivacyLevel.public : IOSLogPrivacyLevel.private ), androidConfig: AndroidConfig( logPrivacyLevel: kDebugMode ? AndroidLogPrivacyLevel.public : AndroidLogPrivacyLevel.private ) ); ``` ## Log privacy levels For better security in production environments, you can control the visibility of log contents using privacy levels. This is especially useful when you need to debug a release build without exposing sensitive information like channel IDs, named users, or custom event data. ### Private (default) This is the default setting, designed to protect data in a production environment. Sensitive information is redacted from log output. **Use case**: Production builds where log output might be collected or viewed by support teams. ### Public This setting increases log visibility, making it easier to capture detailed information from release builds. Sensitive information is visible in logs. To ensure visibility in production builds, `verbose` and `debug` messages are automatically elevated to the `info` log level. **Use case**: Development builds or when actively debugging issues in test environments. > **Warning:** Only use `public` privacy level in development or controlled test environments. Never use it in production builds that will be distributed to end users, as it may expose sensitive user data in logs. ## Viewing logs View Airship SDK logs in your platform's native development tools. ### iOS logs View iOS logs in Xcode's console: 1. Run your app in Xcode 2. Open the **Debug area** (View → Debug Area → Show Debug Area or ⇧⌘Y) 3. Filter logs by typing "Airship" in the search field ### Android logs View Android logs using Logcat: 1. Run your app in Android Studio 2. Open **Logcat** (View → Tool Windows → Logcat) 3. Filter logs by selecting your app's package or searching for "Airship" Or use the command line: `adb logcat | grep Airship` ## Example log output With verbose logging enabled, you'll see detailed output like: ``` [Airship] Airship takeOff succeeded [Airship] Channel ID: 01234567-89ab-cdef-0123-456789abcdef [Airship] Push notifications enabled: true [Airship] Analytics tracking event: screen_viewed ``` ## Troubleshooting logging issues If you're not seeing expected log output: * **Verify log level**: Ensure you've set an appropriate log level (e.g., `LogLevel.verbose` for debugging) * **Check privacy level**: If using release builds, set privacy level to `public` for full visibility * **Filter console output**: Use "Airship" as a filter keyword in your IDE's console * **Restart the app**: Log configuration is set during `takeOff`, so restart the app after making changes # Locale > Configure locale behavior and override the default locale that Airship uses for messaging and analytics. The Airship SDK is localized in 48 different languages for all strings included within the SDK. By default, the SDK automatically uses the device's or app's configured [Locale](https://www.airship.com/docs/reference/glossary/#locale). Airship uses the locale for various locale-sensitive operations: * **Message localization**: Selecting the appropriate language variant for push notifications, in-app messages, and Message Center content * **Analytics reporting**: Recording the user's locale for segmentation and reporting * **SDK strings**: Displaying SDK-generated UI elements in the user's language Apps can override the locale to use a different locale than the device's current setting. This is useful when your app provides its own language selector or needs to match Airship's locale to your app's locale setting. ## Override the locale You can override the locale programmatically at runtime, which takes precedence over the device's locale settings. Use standard locale identifiers (e.g., "en", "en-US", "de", "fr-CA"). ```dart // Set locale to German Airship.locale.setLocaleOverride("de"); // Set locale to Canadian French Airship.locale.setLocaleOverride("fr-CA"); // Set locale to US English Airship.locale.setLocaleOverride("en-US"); ``` > **Note:** Locale overrides persist across app sessions. Once set, the override remains active until explicitly cleared or changed. ## Clear the locale override To remove a locale override and return to using the device's locale: ```dart Airship.locale.clearLocaleOverride(); ``` ## Get the current locale To retrieve the locale that Airship is currently using: ```dart var locale = await Airship.locale.locale; debugPrint("Current Airship locale: $locale"); ``` ## Example: Sync with app language If your app provides a language selector, you can sync Airship's locale with the user's selection: ```dart // When user changes language in your app void onLanguageChanged(String languageCode) { // Update your app's locale // ... your app-specific code ... // Update Airship's locale to match Airship.locale.setLocaleOverride(languageCode); debugPrint("Language changed to: $languageCode"); } // When user resets to device default void onResetToDeviceLanguage() { // Clear Airship's locale override Airship.locale.clearLocaleOverride(); debugPrint("Reset to device locale"); } ``` # Advanced Configuration > Configure URL allowlists and other advanced settings for the Airship Flutter SDK. This guide covers advanced configuration options for the Airship Flutter SDK. ## URL Allowlist The URL allowlist is a security feature that controls which URLs the Airship SDK can open or interact with. This prevents malicious or unintended URLs from being accessed through your app. The SDK divides URL usage into three different config options: * **urlAllowListScopeOpenUrl**: Controls URLs that can be opened from actions, landing pages, HTML in-app messages, or In-App Automation media. By default, allows Airship-originated URLs and YouTube URLs. * **urlAllowListScopeJavaScriptInterface**: Controls which URLs can have the Airship JavaScript interface injected into webviews. By default, allows only Airship-originated URLs. * **urlAllowList**: Applies both scopes to the specified URLs. ### URL pattern syntax ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` ### Example allowlist configurations ```dart var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, // Allow all URLs (use with caution in production) urlAllowList: ["*"] ); ``` ```dart // Allow specific domains var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, urlAllowList: [ "https://yourwebsite.com", "https://*.yourwebsite.com/*" // All subdomains ] ); ``` ```dart // Allow specific URL patterns with different scopes var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, urlAllowListScopeOpenUrl: [ "https://yourwebsite.com/*", "https://youtube.com/*", "yourapp://*" // Deep link scheme ], urlAllowListScopeJavaScriptInterface: [ "https://yourwebsite.com/*" ] ); ``` > **Warning:** Setting `urlAllowList` to `["*"]` allows the SDK to open any URL. While convenient for development, this should be carefully considered for production environments. Only include specific domains you trust. > **Note:** These config options are passed to `Airship.takeOff()` during SDK initialization. See the [Flutter Setup guide](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/) for complete initialization instructions. # Extend Airship > How to extend the Airship Flutter plugin to access native iOS and Android SDK features not exposed through the Flutter API. You can provide a plugin extender that will be automatically loaded for the app. The extender can be used to modify the Airship config before SDK initialization and to access the underlying native SDK once Airship is ready. This gives the app a chance to customize parts of Airship that are not configurable through the Flutter plugin, such as setting up [iOS Live Activities](https://www.airship.com/docs/developer/sdk-integration/flutter/live-activities/) and [Android Live Updates](https://www.airship.com/docs/developer/sdk-integration/flutter/live-updates/). ## iOS For iOS, create a Swift file named `AirshipPluginExtender.swift` and needs to be included in the main app target. Make sure the class has the `@objc(AirshipPluginExtender)` annotation and inherits `AirshipPluginExtenderProtocol`. ```swift import Foundation import AirshipKit import AirshipFrameworkProxy import ActivityKit @objc(AirshipPluginExtender) public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { public static func onAirshipReady() { // Called when Airship is ready on the MainActor } public static func extendConfig(config: inout AirshipConfig) { // Called to extend the AirshipConfig before SDK initialization } } ``` ## Android Create a file in the App's src directory named `AirshipExtender`. It needs to extend `com.urbanairship.android.framework.proxy.AirshipPluginExtender` and have an empty constructor. ```kotlin // Replace with your package package com.example import android.content.Context import androidx.annotation.Keep import com.urbanairship.AirshipConfigOptions import com.urbanairship.UAirship import com.urbanairship.android.framework.proxy.AirshipPluginExtender @Keep public final class AirshipExtender: AirshipPluginExtender { override fun onAirshipReady(context: Context, airship: UAirship) { // Called when Airship is ready on a background thread. // Avoid doing long running, blocking work or it will delay Airship } override fun extendConfig( context: Context, configBuilder: AirshipConfigOptions.Builder ): AirshipConfigOptions.Builder { // Called to extend the AirshipConfig before SDK initialization return configBuilder } } ``` Register the extender in the manifest: ```xml ``` ## Push Notifications Configure and handle push notifications in your Flutter app for iOS and Android. # Push Notifications > Set up push notifications for Flutter applications on iOS and Android. Before you can send and receive push notifications, you need to configure your app for the platform(s) you're targeting. ## Platform Setup Follow the platform-specific setup instructions below to enable push notifications in your Flutter app. ### iOS Configure iOS capabilities and extensions to enable push notifications. #### Enable Push Notifications Capability 1. Open your Flutter project's iOS module in Xcode: * Navigate to your Flutter project directory * Run `open ios/Runner.xcworkspace` (or open the workspace file manually) 2. Select your app target in Xcode, then go to **Signing & Capabilities**. 3. If you do not see Push Notifications enabled, click **+ Capability** and add **Push Notifications**. ![Adding the Push Notifications capability in Xcode](https://www.airship.com/docs/images/ios-enable-push-notifications-capabilities_hu_2e1789fffb02612b.webp) *Adding the Push Notifications capability in Xcode* #### Enable Background Modes 1. Select your app target and then click the **Signing & Capabilities** tab. 2. Click **+ Capability** and add **Background Modes**. ![Adding the Background Modes capability in Xcode](https://www.airship.com/docs/images/ios-enable-background-mode-capabilities_hu_f135d9fec0ba0d06.webp) *Adding the Background Modes capability in Xcode* 3. In the **Background Modes** section, select the **Remote notifications** checkbox. ![Enabling Remote notifications in Background Modes](https://www.airship.com/docs/images/ios-background-mode-remote-notifications_hu_7e38b08288fcd7b2.webp) *Enabling Remote notifications in Background Modes* #### Notification Service Extension

To take advantage of notification attachments, such as images, animated gifs, and video, you will need to create a notification service extension.

Follow the steps in the [iOS Notification Service Extension Guide](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/). ### Android Configure Firebase Cloud Messaging (FCM) or Huawei Mobile Services (HMS) to enable push notifications on Android. #### FCM Setup 1. If you haven't already, create a Firebase project and add your Android app in the [Firebase Console](https://console.firebase.google.com/). 2. Download the `google-services.json` configuration file from your Firebase project. 3. Place `google-services.json` in your Flutter project at `android/app/google-services.json`. #### Configure Gradle 1. Add the Google Services plugin to your project-level `build.gradle` file (`android/build.gradle`): ```gradle buildscript { dependencies { // Add this line classpath 'com.google.gms:google-services:4.3.15' } } ``` 2. Apply the Google Services plugin in your app-level `build.gradle` file (`android/app/build.gradle`): ```gradle apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'dev.flutter.flutter-gradle-plugin' // Add this line at the bottom apply plugin: 'com.google.gms.google-services' ``` #### Notification Configuration Configure the notification icon and accent color in your `takeOff` config: ```dart var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, androidConfig: AndroidConfig( notificationConfig: AndroidNotificationConfig( icon: "ic_notification", accentColor: "#00ff00" ) ) ); 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. ## Enable User Notifications By default, user notifications are disabled to give you control over when to request permission. ### Request notification permission Enable user notifications to prompt for permission: ```dart // Enable user notifications (prompts for permission) await Airship.push.setUserNotificationsEnabled(true); ``` ### When to request permission **Don't prompt immediately**: Requesting notification permission on app launch typically results in low opt-in rates because users don't yet understand your app's value. **Wait for context**: Request permission when users understand the benefit. Good times include: * After completing onboarding * When a user wants to be notified about specific content * After a user takes an action that would benefit from notifications * When explaining the value notifications provide ### Example: Contextual permission request ```dart Future requestNotificationPermission(BuildContext context) async { // Show a dialog explaining the value of notifications bool? shouldEnable = await showDialog( context: context, builder: (context) => AlertDialog( title: Text('Stay Updated'), content: Text( 'Enable notifications to receive important updates ' 'and special offers directly on your device.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text('Not Now'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text('Enable'), ), ], ), ); if (shouldEnable == true) { await Airship.push.setUserNotificationsEnabled(true); } } ``` > **Note:** **Android 13+ (API 33)**: On Android 13 and above, enabling user notifications displays a runtime permission prompt. If users deny permission, they must manually enable notifications in system settings. > > To maximize opt-in rates, avoid prompting immediately on app startup and instead wait for an appropriate moment when users understand the value of notifications. ## Check Notification Status Check if notifications are currently enabled on Airship: ```dart bool enabled = await Airship.push.isUserNotificationsEnabled; print('User notifications enabled: $enabled'); ``` For a detailed breakdown of notification status: ```dart PushNotificationStatus? status = await Airship.push.notificationStatus; print('User notifications enabled: ${status?.isUserNotificationsEnabled}'); print('System notifications allowed: ${status?.areNotificationsAllowed}'); print('Push privacy feature enabled: ${status?.isPushPrivacyFeatureEnabled}'); print('Push token registered: ${status?.isPushTokenRegistered}'); print('Fully opted in: ${status?.isOptedIn}'); ``` The notification status provides: * `isUserNotificationsEnabled`: If user notifications are enabled on Airship * `areNotificationsAllowed`: If notifications are allowed at the system level * `isPushPrivacyFeatureEnabled`: If the push feature is enabled on Privacy Manager * `isPushTokenRegistered`: If push registration was able to generate a token * `isOptedIn`: If Airship is able to send and display push notifications (requires all of the above) See the [Troubleshooting](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/#push-notification-issues) guide for help diagnosing notification issues. ## Get Push Token For debugging or integration purposes, you can access the device's push token: ```dart // Get the push token (FCM registration token on Android, APNs device token on iOS) String? pushToken = await Airship.push.registrationToken; print('Push token: $pushToken'); ``` Listen for token changes: ```dart Airship.push.onPushTokenReceived.listen((event) { print('Push token received: ${event.pushToken}'); }); ``` ## Next Steps * [**Handling Notification Events**](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/handling-notification-events/) — Listen for push received and notification response events * [**Customizing Notifications**](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/customizing-notifications/) — Configure iOS notification options, badges, and foreground presentation If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/flutter/troubleshooting/push-notifications/) to check notification status and fix common issues. # Notification Events > Learn how to listen for push received events, notification responses, and implement background message handling. The Airship SDK provides event streams to listen for push notifications and user interactions. These callbacks allow you to perform custom processing, navigate to specific content, or update your app's state. ## Listen for Push Received Events The `onPushReceived` stream fires when a push notification is received while your app is in the foreground or background: ```dart import 'dart:async'; import 'package:airship_flutter/airship_flutter.dart'; StreamSubscription? _pushSubscription; @override void initState() { super.initState(); // Listen for push notifications _pushSubscription = Airship.push.onPushReceived.listen((event) { print('Push received:'); print('Alert: ${event.pushPayload.alert}'); print('Extras: ${event.pushPayload.extras}'); // Handle the push notification // e.g., show an in-app banner, update UI, etc. }); } @override void dispose() { _pushSubscription?.cancel(); super.dispose(); } ``` > **Note:** **Android**: The `onPushReceived` listener is not called when the app is terminated. For terminated app states on Android, use the [background message handler](#android-background-message-handler) instead. ## Listen for Notification Responses The `onNotificationResponse` stream fires when a user interacts with a notification (taps it, taps an action button, or dismisses it): ```dart StreamSubscription? _responseSubscription; @override void initState() { super.initState(); // Listen for notification interactions _responseSubscription = Airship.push.onNotificationResponse.listen((event) { print('Notification tapped:'); print('Action ID: ${event.actionId}'); // null for default tap print('Alert: ${event.pushPayload.alert}'); // Navigate to specific content based on the push if (event.pushPayload.extras['deep_link'] != null) { String deepLink = event.pushPayload.extras['deep_link']; _navigateToDeepLink(deepLink); } }); } void _navigateToDeepLink(String deepLink) { // Navigate to the appropriate screen Navigator.pushNamed(context, deepLink); } @override void dispose() { _responseSubscription?.cancel(); super.dispose(); } ``` ## Listen for Notification Status Changes Monitor changes to the notification status: ```dart StreamSubscription? _statusSubscription; @override void initState() { super.initState(); _statusSubscription = Airship.push.onNotificationStatusChanged.listen((event) { print('Notification status changed:'); print('Opted in: ${event.status.isOptedIn}'); print('System allowed: ${event.status.areNotificationsAllowed}'); }); } @override void dispose() { _statusSubscription?.cancel(); super.dispose(); } ``` ## Complete Example Here's a complete example showing how to handle both push received and notification response events: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; import 'dart:async'; class NotificationHandler extends StatefulWidget { @override _NotificationHandlerState createState() => _NotificationHandlerState(); } class _NotificationHandlerState extends State { StreamSubscription? _pushSubscription; StreamSubscription? _responseSubscription; String _lastNotification = 'No notifications yet'; @override void initState() { super.initState(); _setupNotificationListeners(); } void _setupNotificationListeners() { // Handle push received (foreground/background) _pushSubscription = Airship.push.onPushReceived.listen((event) { setState(() { _lastNotification = 'Received: ${event.pushPayload.alert ?? "No alert"}'; }); // Show an in-app notification ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(event.pushPayload.alert ?? 'New notification')), ); }); // Handle notification interaction _responseSubscription = Airship.push.onNotificationResponse.listen((event) { setState(() { _lastNotification = 'Tapped: ${event.pushPayload.alert ?? "No alert"}'; }); // Handle deep links or custom actions Map extras = event.pushPayload.extras; if (extras.containsKey('screen')) { Navigator.pushNamed(context, extras['screen']); } else if (extras.containsKey('url')) { // Open URL with url_launcher package // launch(extras['url']); } }); } @override void dispose() { _pushSubscription?.cancel(); _responseSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Notifications')), body: Center( child: Text(_lastNotification), ), ); } } ``` ## Android Background Message Handler For Android, you must set up a background message handler to receive push notifications when the app is completely terminated (not running in the foreground or background). ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; // Background message handler (must be a top-level function) Future backgroundMessageHandler(PushReceivedEvent event) async { print('Received background push:'); print('Alert: ${event.pushPayload.alert}'); print('Extras: ${event.pushPayload.extras}'); // Perform background work // Note: Keep this lightweight and fast // Avoid UI operations or heavy processing } void main() { WidgetsFlutterBinding.ensureInitialized(); // Register the background message handler (Android only) Airship.push.android.setBackgroundPushReceivedHandler(backgroundMessageHandler); // Initialize Airship var config = AirshipConfig( defaultEnvironment: ConfigEnvironment( appKey: "YOUR_APP_KEY", appSecret: "YOUR_APP_SECRET" ), site: Site.us, ); Airship.takeOff(config); runApp(MyApp()); } ``` > **Important:** **Background handler requirements:** > * Must be a top-level function (not a class method) > * Should complete quickly (within a few seconds) > * Avoid UI operations or long-running tasks > * Cannot access `BuildContext` or app state directly > * Only applies to Android (iOS handles background notifications differently) ## Working with Push Payloads The `PushPayload` object contains all notification data: ```dart Airship.push.onNotificationResponse.listen((event) { PushPayload payload = event.pushPayload; // Standard fields print('Alert: ${payload.alert}'); print('Title: ${payload.title}'); print('Subtitle: ${payload.subtitle}'); print('Notification ID: ${payload.notificationId}'); // Custom data Map extras = payload.extras; if (extras.containsKey('product_id')) { String productId = extras['product_id']; // Navigate to product details } }); ``` # Customize Notifications > Configure iOS notification options, badges, foreground presentation, and silent notifications. Customize how notifications appear and behave on iOS and Android platforms. ## iOS Notification Options By default, the Airship SDK will request `Alert`, `Badge`, and `Sound` notification options for remote notifications. You can customize these options by setting them before enabling user notifications: ```dart Airship.push.iOS.setNotificationOptions([ IOSNotificationOption.alert, IOSNotificationOption.badge, IOSNotificationOption.sound, ]); // Then enable user notifications await Airship.push.setUserNotificationsEnabled(true); ``` ### Available Options * `IOSNotificationOption.alert` - Display alerts * `IOSNotificationOption.badge` - Update the app badge * `IOSNotificationOption.sound` - Play sounds * `IOSNotificationOption.carPlay` - Display notifications in CarPlay * `IOSNotificationOption.criticalAlert` - Display critical alerts (requires special entitlement) * `IOSNotificationOption.providesAppNotificationSettings` - Indicates the app has custom notification settings * `IOSNotificationOption.provisional` - Enables provisional authorization ## Provisional Authorization Apps can request provisional authorization along with the usual notification options. When requesting provisional authorization, apps do not need to prompt the user for permission initially, and notifications will be delivered in a non-interruptive manner to the Notification Center until the user explicitly chooses to keep delivering messages either prominently or quietly. ```dart Airship.push.iOS.setNotificationOptions([ IOSNotificationOption.alert, IOSNotificationOption.badge, IOSNotificationOption.sound, IOSNotificationOption.provisional, ]); // Enable notifications (no prompt will be shown) await Airship.push.setUserNotificationsEnabled(true); ``` > **Note:** Provisional notifications appear in Notification Center but not as banners or with sounds. Users can then choose to keep or turn off notifications from Notification Center. ## iOS Foreground Presentation Options When a push is received in the foreground on iOS, how the notification is displayed to the user is controlled by foreground presentation options. By default, the SDK will not set any options so the notification will be silenced. ```dart Airship.push.iOS.setForegroundPresentationOptions([ IOSForegroundPresentationOption.banner, IOSForegroundPresentationOption.list, IOSForegroundPresentationOption.sound, ]); ``` ### Available Presentation Options * `IOSForegroundPresentationOption.sound` - Play the sound associated with the notification * `IOSForegroundPresentationOption.badge` - Apply the notification's badge value to the app's icon * `IOSForegroundPresentationOption.list` - Show the notification in Notification Center (and as a banner on iOS 13 and older) * `IOSForegroundPresentationOption.banner` - Present the notification as a banner (and in Notification Center on iOS 13 and older) > **Note:** If no foreground presentation options are set, notifications received while the app is in the foreground will be silently received without displaying any UI to the user. ## iOS Badge Management The badge on iOS presents a counter on top of the application icon. You can control this directly through Airship: ```dart // Set badge number await Airship.push.iOS.setBadge(20); // Get current badge number int currentBadge = await Airship.push.iOS.badge; print('Current badge: $currentBadge'); // Reset badge await Airship.push.iOS.resetBadge(); ``` ### Auto-Badge Auto-badge automatically increments the badge when a notification is received and decrements it when the notification is cleared: ```dart // Enable auto-badge await Airship.push.iOS.setAutoBadgeEnabled(true); // Check if auto-badge is enabled bool isEnabled = await Airship.push.iOS.isAutoBadgeEnabled(); ``` > **Important:** When using auto-badge, only modify the badge value through Airship methods to ensure the value stays in sync. ## iOS Authorized Notification Settings Check which notification settings the user has authorized: ```dart // Get authorized notification settings List settings = await Airship.push.iOS.authorizedNotificationSettings; print('Authorized settings: $settings'); // Get authorization status IOSAuthorizedNotificationStatus status = await Airship.push.iOS.authorizedNotificationStatus; print('Authorization status: $status'); ``` Listen for changes to authorized settings: ```dart Airship.push.iOS.onAuthorizedSettingsChanged.listen((event) { print('Authorized settings changed: ${event.authorizedSettings}'); }); ``` ## Silent Notifications Silent notifications are push messages that do not present a notification to the user. These are typically used to briefly wake the app from a background state to perform processing tasks or fetch remote content. > **Important:** We recommend that you thoroughly test your implementation to confirm that silent notifications do not generate any device notifications. ### Platform-Specific Behavior * **Android**: All push messages are delivered in the background. By default, Airship treats messages without an `alert` as silent. * **iOS**: Set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject) when sending the push. ### Example Silent Notification Payload ```json { "audience": "all", "notification": { "ios": { "content_available": true, "extra": { "update_type": "content_refresh" } }, "android": { "extra": { "update_type": "content_refresh" } } }, "device_types": ["ios", "android"] } ``` > **Note:** Pushes sent with the `content_available` property (iOS) or without an `alert` (Android) do not have guaranteed delivery. Factors affecting delivery include battery life, whether the device is connected to WiFi, and the number of silent pushes sent within a recent time period. These metrics are determined solely by iOS/Android and APNs/FCM. Therefore, this feature is best used for supplementing the regular behavior of the app rather than providing critical functionality. For instance, an app could use a silent push to pre-fetch new data ahead of time in order to reduce load times when the app is later launched by the user. ## Clear Notifications Clear notifications programmatically: ```dart // Clear all notifications await Airship.push.clearNotifications(); // Clear a specific notification by ID await Airship.push.clearNotification('notification_id'); // Get active notifications List activeNotifications = await Airship.push.activeNotifications; print('Active notifications: ${activeNotifications.length}'); ``` > **Note:** On Android, this only clears notifications sent through Airship. On iOS, it clears all notifications for the app. ## 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. ## Message Center Implement Message Center in your Flutter app to provide an inbox for persistent, rich messages. # Message Center > Learn how to display Message Center, manage messages, and implement common inbox functionality in your Flutter app. Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center The Flutter plugin provides a simple way to display the built-in Message Center UI. ### Using the default UI Display the built-in Message Center UI with a single method call. This is perfect for quickly adding Message Center functionality without custom design work: ```dart // Display the default Message Center UI Airship.messageCenter.display(); ``` This method can be called from anywhere in your app: ```dart // Example: Add a button to your app bar AppBar( title: Text('My App'), actions: [ IconButton( icon: Icon(Icons.inbox), onPressed: () { Airship.messageCenter.display(); }, ), ], ) ``` ### Display a specific message You can also display a specific message directly by providing its message ID: ```dart Airship.messageCenter.display(messageId: "specific-message-id"); ``` To customize the Message Center UI or navigation, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/flutter/message-center/embedding/). ## Fetch messages Retrieve all messages from the inbox: ```dart List messages = await Airship.messageCenter.messages; // Display messages in your UI for (var message in messages) { print('Message: ${message.title}'); print('ID: ${message.id}'); print('Unread: ${message.unread}'); print('Sent date: ${message.sentDate}'); } ``` ### InboxMessage properties Each `InboxMessage` contains: * **id**: Unique identifier for the message * **title**: Message title * **sentDate**: When the message was sent * **expirationDate**: When the message expires (optional) * **unread**: Whether the message is unread * **extras**: Custom key-value pairs associated with the message ## Listen for message updates Subscribe to real-time message updates using streams. This is useful for updating your UI when new messages arrive or existing messages are modified: ```dart StreamSubscription? inboxSubscription; @override void initState() { super.initState(); // Listen for inbox updates inboxSubscription = Airship.messageCenter.onInboxUpdated.listen((event) { setState(() { // Reload messages when the inbox is updated _loadMessages(); }); }); } Future _loadMessages() async { List messages = await Airship.messageCenter.messages; setState(() { _messages = messages; }); } @override void dispose() { inboxSubscription?.cancel(); super.dispose(); } ``` ## Track unread count Monitor the unread message count to display badges or update UI elements: ### Get current unread count ```dart int unreadCount = await Airship.messageCenter.unreadCount; print('Unread messages: $unreadCount'); ``` ### Listen for unread count changes ```dart // This is useful for updating badges in real-time Airship.messageCenter.onInboxUpdated.listen((event) async { int unreadCount = await Airship.messageCenter.unreadCount; // Update your badge UI _updateBadge(unreadCount); }); ``` ### Example: Show unread badge ```dart class InboxButton extends StatefulWidget { @override _InboxButtonState createState() => _InboxButtonState(); } class _InboxButtonState extends State { int _unreadCount = 0; StreamSubscription? _subscription; @override void initState() { super.initState(); _loadUnreadCount(); // Update badge when inbox changes _subscription = Airship.messageCenter.onInboxUpdated.listen((_) { _loadUnreadCount(); }); } Future _loadUnreadCount() async { int count = await Airship.messageCenter.unreadCount; setState(() { _unreadCount = count; }); } @override void dispose() { _subscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return IconButton( icon: Badge( label: _unreadCount > 0 ? Text('$_unreadCount') : null, child: Icon(Icons.inbox), ), onPressed: () { Airship.messageCenter.display(); }, ); } } ``` ## Refresh messages Manually refresh the message list from the server. This is useful for implementing pull-to-refresh functionality: ```dart // Refresh messages bool success = await Airship.messageCenter.refreshInbox(); if (success) { print('Messages refreshed successfully'); } else { print('Failed to refresh messages'); } ``` ### Example: Pull-to-refresh ```dart RefreshIndicator( onRefresh: () async { await Airship.messageCenter.refreshInbox(); }, child: ListView.builder( itemCount: messages.length, itemBuilder: (context, index) { return MessageListItem(message: messages[index]); }, ), ) ``` ## Mark messages as read Mark one or more messages as read to update their status: ```dart // Mark a single message as read await Airship.messageCenter.markRead(message.id); // Or using the message object directly await Airship.messageCenter.markRead(message.id); ``` Messages are typically marked as read automatically when viewed in the default Message Center UI. For custom implementations, you should mark messages as read when the user views them: ```dart // When user taps on a message void onMessageTapped(InboxMessage message) { // Mark as read Airship.messageCenter.markRead(message.id); // Navigate to message detail screen Navigator.push( context, MaterialPageRoute( builder: (context) => MessageDetailScreen(message: message), ), ); } ``` ## Delete messages Delete messages from the inbox: ```dart // Delete a single message await Airship.messageCenter.deleteMessage(message.id); ``` ### Example: Swipe to delete ```dart Dismissible( key: Key(message.id), direction: DismissDirection.endToStart, onDismissed: (direction) { Airship.messageCenter.deleteMessage(message.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Message deleted')), ); }, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete, color: Colors.white), ), child: MessageListItem(message: message), ) ``` ## Filter messages by named user By default, Message Center displays all messages sent to the device's channel. If multiple users log into your app on the same device, they'll all see the same messages. To filter messages by named user, you need to: 1. Include a custom key when creating messages with `named_user_id` as the key and the user's actual ID as the value 2. Implement filtering logic in your custom Message Center implementation When creating Message Center messages: * **For the API**: Use the `extra` object in the [Message Center object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#messageobject) * **In the dashboard**: See [Add custom keys](https://www.airship.com/docs/guides/messaging/messages/content/app/message-center/#add-custom-keys) in the Message Center content guide Then filter messages in your Flutter code: ```dart Future> getMessagesForCurrentUser() async { // Get the current named user ID String? currentUserId = await Airship.contact.namedUserId; if (currentUserId == null) { return []; // No user logged in } // Get all messages List allMessages = await Airship.messageCenter.messages; // Filter messages that match the current user return allMessages.where((message) { // Check if the message has a named_user_id extra String? messageUserId = message.extras['named_user_id']; return messageUserId == null || messageUserId == currentUserId; }).toList(); } ``` # Embed the Message Center > Build custom Message Center UIs with full control over design, navigation, and functionality using the InboxMessageView widget and Message Center APIs. This guide covers creating fully custom Message Center implementations for Flutter applications, giving you complete control over the design, navigation, and user experience. ## Override Default Display Behavior To use a custom Message Center implementation instead of the default UI, disable auto-launch and listen for display events: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); // Disable the default Message Center UI Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); // Listen for display events and navigate to custom UI Airship.messageCenter.onDisplay.listen((event) { // Navigate to your custom Message Center screen Navigator.push( context, MaterialPageRoute( builder: (context) => CustomMessageCenterScreen( messageId: event.messageId, // null for full inbox, or specific message ID ), ), ); }); } @override Widget build(BuildContext context) { return MaterialApp( home: HomeScreen(), ); } } ``` > **Note:** When you disable the default Message Center, you're responsible for displaying your own UI when the `onDisplay` event fires. This includes handling both full inbox views and individual message views. ## Why customize Message Center While the default Message Center UI works great for many apps, you might want to customize it for: * **Brand consistency**: Match your app's unique design language and visual style * **Custom navigation**: Integrate Message Center into your app's existing navigation patterns * **Enhanced functionality**: Add search, filtering, categorization, or other custom features * **Multi-user support**: Filter messages by named user when multiple users share a device * **Platform-specific design**: Create different experiences for iOS and Android ## Custom Message Center implementation To build a custom Message Center, disable the default UI and use the Message Center APIs to manage messages programmatically. ### Step 1: Disable the default UI First, disable the default Message Center so you can display your own: ```dart // In your app initialization Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); ``` ### Step 2: Listen for display events Handle display events to show your custom UI when Message Center is triggered: ```dart Airship.messageCenter.onDisplay.listen((event) { // event.messageId will be null for full inbox, or contain a specific message ID Navigator.push( context, MaterialPageRoute( builder: (context) => CustomMessageCenterScreen( messageId: event.messageId, ), ), ); }); ``` ### Step 3: Build your custom UI Create your custom Message Center screen using the Message Center APIs: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; import 'dart:async'; class CustomMessageCenterScreen extends StatefulWidget { final String? messageId; const CustomMessageCenterScreen({Key? key, this.messageId}) : super(key: key); @override _CustomMessageCenterScreenState createState() => _CustomMessageCenterScreenState(); } class _CustomMessageCenterScreenState extends State { List _messages = []; int _unreadCount = 0; bool _isLoading = true; StreamSubscription? _inboxSubscription; @override void initState() { super.initState(); _loadMessages(); // Listen for inbox updates _inboxSubscription = Airship.messageCenter.onInboxUpdated.listen((_) { _loadMessages(); }); } Future _loadMessages() async { setState(() { _isLoading = true; }); try { List messages = await Airship.messageCenter.messages; int unreadCount = await Airship.messageCenter.unreadCount; setState(() { _messages = messages; _unreadCount = unreadCount; _isLoading = false; }); // If a specific message was requested, open it if (widget.messageId != null) { InboxMessage? message = _messages.firstWhere( (m) => m.id == widget.messageId, orElse: () => null as InboxMessage, ); if (message != null) { _openMessage(message); } } } catch (e) { setState(() { _isLoading = false; }); print('Error loading messages: $e'); } } Future _refreshMessages() async { await Airship.messageCenter.refreshInbox(); } void _openMessage(InboxMessage message) { // Mark as read Airship.messageCenter.markRead(message.id); // Navigate to message detail Navigator.push( context, MaterialPageRoute( builder: (context) => MessageDetailScreen(message: message), ), ); } Future _deleteMessage(InboxMessage message) async { await Airship.messageCenter.deleteMessage(message.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Message deleted')), ); } @override void dispose() { _inboxSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Messages'), actions: [ if (_unreadCount > 0) Center( child: Padding( padding: EdgeInsets.only(right: 16), child: Chip( label: Text('$_unreadCount unread'), backgroundColor: Theme.of(context).primaryColor, labelStyle: TextStyle(color: Colors.white), ), ), ), ], ), body: _isLoading ? Center(child: CircularProgressIndicator()) : _messages.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox, size: 64, color: Colors.grey), SizedBox(height: 16), Text( 'No messages', style: Theme.of(context).textTheme.headlineSmall, ), ], ), ) : RefreshIndicator( onRefresh: _refreshMessages, child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { InboxMessage message = _messages[index]; return Dismissible( key: Key(message.id), direction: DismissDirection.endToStart, onDismissed: (direction) { _deleteMessage(message); }, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete, color: Colors.white), ), child: ListTile( leading: CircleAvatar( backgroundColor: message.unread ? Theme.of(context).primaryColor : Colors.grey, child: Icon( message.unread ? Icons.mail : Icons.drafts, color: Colors.white, ), ), title: Text( message.title ?? 'No title', style: TextStyle( fontWeight: message.unread ? FontWeight.bold : FontWeight.normal, ), ), subtitle: Text( _formatDate(message.sentDate), style: TextStyle(fontSize: 12), ), trailing: Icon(Icons.chevron_right), onTap: () => _openMessage(message), ), ); }, ), ), ); } String _formatDate(DateTime date) { DateTime now = DateTime.now(); Duration difference = now.difference(date); if (difference.inDays == 0) { return 'Today ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; } else if (difference.inDays == 1) { return 'Yesterday'; } else if (difference.inDays < 7) { return '${difference.inDays} days ago'; } else { return '${date.month}/${date.day}/${date.year}'; } } } ``` ## Using the InboxMessageView widget The `InboxMessageView` widget displays individual Message Center messages with their HTML content. Use this widget to create custom message detail screens: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class MessageDetailScreen extends StatefulWidget { final InboxMessage message; const MessageDetailScreen({Key? key, required this.message}) : super(key: key); @override _MessageDetailScreenState createState() => _MessageDetailScreenState(); } class _MessageDetailScreenState extends State { InboxMessageViewController? _controller; void _onInboxMessageViewCreated(InboxMessageViewController controller) { _controller = controller; controller.loadMessage(widget.message); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.message.title ?? 'Message'), actions: [ IconButton( icon: Icon(Icons.delete), onPressed: () { // Delete and go back Airship.messageCenter.deleteMessage(widget.message.id); Navigator.pop(context); }, ), ], ), body: InboxMessageView( onViewCreated: _onInboxMessageViewCreated, ), ); } } ``` ### InboxMessageView properties The `InboxMessageView` widget provides: * **Automatic HTML rendering**: Displays rich HTML content from Message Center messages * **Link handling**: Automatically handles links within message content * **Action execution**: Processes Airship actions embedded in messages * **Responsive layout**: Adapts to different screen sizes ## Advanced: Message filtering Filter messages based on custom criteria, such as named user or categories: ```dart class FilteredMessageCenterScreen extends StatefulWidget { @override _FilteredMessageCenterScreenState createState() => _FilteredMessageCenterScreenState(); } class _FilteredMessageCenterScreenState extends State { List _filteredMessages = []; String? _currentCategory; Future _loadAndFilterMessages() async { // Get all messages List allMessages = await Airship.messageCenter.messages; // Get current named user String? namedUserId = await Airship.contact.namedUserId; // Filter messages List filtered = allMessages.where((message) { // Filter by named user if set if (namedUserId != null) { String? messageUserId = message.extras['named_user_id']; if (messageUserId != null && messageUserId != namedUserId) { return false; } } // Filter by category if set if (_currentCategory != null) { String? messageCategory = message.extras['category']; if (messageCategory != _currentCategory) { return false; } } return true; }).toList(); setState(() { _filteredMessages = filtered; }); } void _setCategory(String? category) { setState(() { _currentCategory = category; }); _loadAndFilterMessages(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Messages'), actions: [ PopupMenuButton( onSelected: _setCategory, itemBuilder: (context) => [ PopupMenuItem(value: null, child: Text('All')), PopupMenuItem(value: 'promotions', child: Text('Promotions')), PopupMenuItem(value: 'updates', child: Text('Updates')), PopupMenuItem(value: 'alerts', child: Text('Alerts')), ], ), ], ), body: ListView.builder( itemCount: _filteredMessages.length, itemBuilder: (context, index) { return MessageListItem(message: _filteredMessages[index]); }, ), ); } } ``` ## Best practices When implementing custom Message Center: 1. **Always mark messages as read**: When a user views a message, mark it as read to keep the unread count accurate 2. **Handle empty states**: Show helpful messages when the inbox is empty 3. **Implement pull-to-refresh**: Let users manually refresh their messages 4. **Show loading indicators**: Provide feedback while fetching messages 5. **Clean up subscriptions**: Cancel stream subscriptions in `dispose()` to prevent memory leaks 6. **Handle errors gracefully**: Show user-friendly error messages if message loading fails 7. **Test with multiple users**: If implementing named user filtering, thoroughly test the filtering logic ## Preference Center Let users manage their notification preferences and subscription lists with Preference Center. # Preference Center > Preference Center allows users to opt in and out of subscription lists configured via the Airship Dashboard. > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. Preference Center provides a pre-built UI for users to manage their subscription preferences. Learn more in the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Display a Preference Center Display a Preference Center with a single method call: ```dart Airship.preferenceCenter.display("preference-center-id"); ``` To build a custom Preference Center UI, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/flutter/preference-center/embedding/). # Embed the Preference Center > Create custom Preference Center UIs by fetching the config and building your own subscription management interface. This guide covers creating custom Preference Center UIs for Flutter applications. Unlike the default Preference Center, you'll build your own UI from scratch using the Preference Center configuration and subscription list APIs. ## Override Default Display Behavior To use a custom Preference Center instead of the default UI, disable auto-launch for the specific Preference Center ID and handle display events: ```dart // Disable the OOTB UI for this Preference Center Airship.preferenceCenter.setAutoLaunchDefaultPreferenceCenter( "preference-center-id", false ); // Add a listener to handle display events StreamSubscription? subscription; subscription = Airship.preferenceCenter.onDisplay.listen((event) { final preferenceCenterId = event.preferenceCenterId; // Navigate to your custom preference center UI navigateToCustomPreferenceCenter(preferenceCenterId); }); ``` ## Fetching Preference Center Config The Preference Center config contains all the information needed to build your UI, including subscription lists, sections, and display settings. ```dart PreferenceCenterConfig config = await Airship.preferenceCenter.getConfig("preference-center-id"); ``` > **Note:** The config might not be available immediately on first app start. Implement exponential backoff if automatically retrying, or provide a UI for users to manually retry. ## Building Your Custom UI You'll need to: 1. **Fetch the config** to get the list of subscription lists and their current state 2. **Build your UI** using the config data (sections, subscription lists, display settings) 3. **Update subscription lists** when users make changes using the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/flutter/audience/subscription-lists/) ### Example Implementation ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class CustomPreferenceCenterScreen extends StatefulWidget { final String preferenceCenterId; const CustomPreferenceCenterScreen({ Key? key, required this.preferenceCenterId, }) : super(key: key); @override _CustomPreferenceCenterScreenState createState() => _CustomPreferenceCenterScreenState(); } class _CustomPreferenceCenterScreenState extends State { PreferenceCenterConfig? _config; bool _loading = true; Map _subscriptions = {}; @override void initState() { super.initState(); _loadConfig(); } Future _loadConfig() async { try { final config = await Airship.preferenceCenter.getConfig( widget.preferenceCenterId, ); // Load current subscription status final currentSubs = await Airship.contact.subscriptionLists; final subsMap = {}; for (var section in config.sections) { for (var item in section.items) { if (item.subscriptionId != null) { subsMap[item.subscriptionId!] = currentSubs.contains(item.subscriptionId); } } } setState(() { _config = config; _subscriptions = subsMap; _loading = false; }); } catch (error) { print('Failed to load config: $error'); setState(() { _loading = false; }); } } Future _toggleSubscription(String listId, bool subscribe) async { try { if (subscribe) { await Airship.contact.editSubscriptionLists() .subscribe(listId) .apply(); } else { await Airship.contact.editSubscriptionLists() .unsubscribe(listId) .apply(); } setState(() { _subscriptions[listId] = subscribe; }); } catch (error) { print('Failed to update subscription: $error'); } } @override Widget build(BuildContext context) { if (_loading) { return Scaffold( appBar: AppBar(title: Text('Loading...')), body: Center(child: CircularProgressIndicator()), ); } if (_config == null) { return Scaffold( appBar: AppBar(title: Text('Error')), body: Center( child: Text('Failed to load Preference Center'), ), ); } return Scaffold( appBar: AppBar( title: Text(_config!.display?.name ?? 'Preferences'), ), body: ListView.builder( itemCount: _config!.sections.length, itemBuilder: (context, sectionIndex) { final section = _config!.sections[sectionIndex]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (section.display?.name != null) Padding( padding: EdgeInsets.all(16), child: Text( section.display!.name!, style: Theme.of(context).textTheme.titleLarge, ), ), ...section.items.map((item) { final subscriptionId = item.subscriptionId; if (subscriptionId == null) return SizedBox.shrink(); return SwitchListTile( title: Text(item.display?.name ?? ''), subtitle: item.display?.description != null ? Text(item.display!.description!) : null, value: _subscriptions[subscriptionId] ?? false, onChanged: (value) { _toggleSubscription(subscriptionId, value); }, ); }).toList(), Divider(), ], ); }, ), ); } } ``` > **Important:** Preference Center configuration is currently limited to subscription lists only. Use the [Subscription List APIs](https://www.airship.com/docs/developer/sdk-integration/flutter/audience/subscription-lists/) to manage user subscriptions. ## Audience Management Integrate audience management features into your Flutter app to identify users, set attributes, manage tags, and control subscription lists for targeted messaging. # Channels > Access and manage channel IDs and listen for channel creation. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. ```dart // Get the channel ID (may return null if not yet created) String? channelId = await Airship.channel.identifier; // Wait for the channel ID to be created (returns the channel ID once available) String channelId = await Airship.channel.waitForChannelId(); ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Use `waitForChannelId()` if you need to wait for the channel to be created before proceeding. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. ```dart Airship.channel.onChannelCreated.listen((event) { debugPrint('Channel created $event'); }); ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfig`, see [Flutter SDK Setup](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/). ```dart Airship.takeOff( AirshipConfig( ... isChannelCaptureEnabled: false ) ); ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during takeOff. For more information about Privacy Manager, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. ```dart Airship.contact.identify(namedUserId); ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. ```dart Airship.contact.reset(); ``` You can get the Named User ID only if you set it through the SDK. ```dart await Airship.contact.namedUserId; ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. ```dart Airship.channel.addTags(["flutter"]); Airship.channel.removeTags(["some-tag"]); // Accessing channel tags List tags = await Airship.channel.tags; ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```dart Airship.channel.editTagGroups() ..addTags("loyalty", ["silver-member"]) ..removeTags("loyalty", ["bronze-member"]) ..apply() ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. ```dart Airship.contact.editTagGroups() ..addTags("loyalty", ["silver-member"]) ..removeTags("loyalty", ["bronze-member"]) ..apply() ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. ```dart Airship.channel.editAttributes() ..setAttribute("device_name", "Bobby's Phone") ..setAttribute("average_rating", 4.99) ..removeAttribute("vip_status") ..apply() ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. ```dart Airship.contact.editAttributes() ..setAttribute("first_name", "Bobby") ..apply() ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. ```dart Airship.contact.editAttributes() ..setJsonAttribute("attribute_name", "instance_id", {"key":"value", "another_key":"another_value"}) ..removeJsonAttribute("some_attribute_name", "some_instance_id") ..apply() ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. ```dart // Modifying channel subscription lists Airship.channel.editSubscriptionLists() ..subscribe("food") ..unsubscribe("sports") ..apply(); // Fetching channel subscription lists List channelSubscriptions = await Airship.channel.subscriptionLists; ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. ```dart // Modifying contact subscription lists Airship.contact.editSubscriptionLists() ..subscribe("food", ChannelScope.app) ..unsubscribe("sports", ChannelScope.sms) ..apply(); // Fetching contact subscription lists Map> contactSubscriptions = await Airship.contact.subscriptionLists; ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ## Data Collection Overview of data collection and controls provided by the Airship Flutter SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: | Privacy Manager Flag | Features | |----------------------|----------| | `push` | Push notifications | | `in_app_automation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | | `message_center` | Message Center | | `tags_and_attributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | | `contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | | `analytics` | Associated identifiers, Custom events, Screen tracking, Surveys, email address, Feature Flag interaction | | `feature_flags` | Feature Flag evaluation and interaction | | `all` | All features | | `none` | No features | ## Configuring default enabled features You can configure which features are enabled by default when the SDK initializes. This is done in your Airship config during `takeOff`. For information about setting up the Airship SDK and configuring `AirshipConfig`, see [Flutter SDK Setup](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/). ### Default configuration (all features enabled) By default, all features are enabled. The SDK will collect data and make network requests as configured. ### Disabling all features To disable all features by default (useful for consent opt-in flows), set the enabled features to an empty array: ```dart Airship.takeOff( AirshipConfig( defaultEnvironment: ConfigEnvironment( enabledFeatures: [] ) ) ); ``` ### Enabling specific features To enable only specific features by default: ```dart Airship.takeOff( AirshipConfig( defaultEnvironment: ConfigEnvironment( enabledFeatures: ["push", "analytics"] ) ) ); ``` ## Modifying enabled features at runtime You can enable or disable features at any time after takeOff. Once you modify the enabled features from the default, those settings are persisted between app launches. ### Enabling features ```dart Airship.privacyManager.enableFeatures(["push", "analytics"]); ``` ### Disabling features ```dart Airship.privacyManager.disableFeatures(["push", "analytics"]); ``` ## Consent opt-in flow example A common use case is to start with all features disabled, then enable them as users grant consent: ```dart // Start with all features disabled Airship.takeOff( AirshipConfig( defaultEnvironment: ConfigEnvironment( enabledFeatures: [] ) ) ); // Later, when user grants consent: void userGrantedConsent() { // Enable features based on user's consent choices Airship.privacyManager.enableFeatures(["push", "analytics"]); } ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Apple Privacy Manifest](https://www.airship.com/docs/reference/data-collection/apple-privacy-manifest/) - Declare data collection practices to Apple (iOS) - [Google Play Data Safety](https://www.airship.com/docs/reference/data-collection/google-play-data-safety/) - Reference for Google Play's Data Safety section (Android) - [Analytics](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/flutter/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with custom events. They require enabling analytics for your app. For detailed information, see the [Custom Events guide](https://www.airship.com/docs/guides/audience/events/custom-events/). ```dart CustomEvent event = CustomEvent( name: "event_name", value: 123.12, properties: { "my_custom_property": "some custom value", "is_neat": true, "any_json": { "foo": "bar" } } ); Airship.analytics.recordCustomEvent(event); ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. ```dart Airship.analytics.associateIdentifier("key", "value"); ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. ```dart Airship.analytics.trackScreen("MainScreen"); ``` ## Troubleshooting Common issues and solutions for Airship plugin setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/) or [Advanced Configuration](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/advanced-configuration/), if you don't see a channel ID in the logs or encounter errors during initialization, review the following common problems and solutions. ## Installation Errors If you encounter errors during installation: - Verify that you're using a compatible version of Flutter. See [Requirements](https://www.airship.com/docs/developer/sdk-integration/flutter/installation/getting-started/#requirements) in *Getting Started*. - Ensure all native dependencies are properly linked. - Run `flutter clean` and `flutter pub get` to refresh dependencies. - For iOS, run `pod install` in the `ios` directory. - Check that your iOS and Android projects are correctly configured. ## Initialization Errors If you encounter errors during SDK initialization: - Verify your credentials in the Airship dashboard. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. - Ensure `WidgetsFlutterBinding.ensureInitialized()` runs first in `main()`, then `Airship.takeOff`, then `runApp()`. - Ensure `site` in your `AirshipConfig` is `Site.us` for US cloud projects or `Site.eu` for EU cloud projects. # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/flutter/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. ## Push Notifications Not Working If push notifications are not being received: - Verify that push notifications are enabled for both iOS and Android. - Check that APNs (iOS) and FCM (Android) are properly configured. - Ensure the app has notification permissions. - For iOS, verify capabilities are enabled in Xcode (Push Notifications and Background Modes). - For Android, ensure `google-services.json` is in `android/app/`. ## Checking Push Notification Status If push notifications aren't working as expected, you can check the notification status to diagnose the issue: ```dart PushNotificationStatus? status = await Airship.push.notificationStatus; print('User notifications enabled: ${status?.isUserNotificationsEnabled}'); print('System notifications allowed: ${status?.areNotificationsAllowed}'); print('Push privacy feature enabled: ${status?.isPushPrivacyFeatureEnabled}'); print('Push token registered: ${status?.isPushTokenRegistered}'); print('User opted in: ${status?.isUserOptedIn}'); print('Fully opted in: ${status?.isOptedIn}'); ``` You can also listen for status changes: ```dart Airship.push.onNotificationStatusChanged.listen((event) { print('Notification status changed: ${event.status}'); print('Is opted in: ${event.status.isOptedIn}'); }); ``` ## Common Status Scenarios - `isUserOptedIn = false`: Check if `userNotificationsEnabled` is set to `true` and if the user granted permission - `isPushPrivacyFeatureEnabled = false`: Push privacy feature is disabled in Privacy Manager - `isPushTokenRegistered = false`: Device hasn't received a push token yet. Check network connectivity and platform configuration - `isUserOptedIn = true` but `isOptedIn = false`: Push token registration is pending or failed. Check console logs for errors