Flutter SDK - Current (26.1.0)
Introduction
The Flutter SDK wraps the Atomic iOS (26.2.0) and Android (26.1.1) SDKs, allowing you to use them in your Flutter apps.
Supported iOS and Android versions
The Flutter SDK supports iOS 16 and above, and Android 7.0 (API 24) and above.
Atomic Flutter SDK is null-safe.
Using Dart SDK >=3.0.0 <4.0.0.
The current stable release is 26.1.0.
If you are upgrading from 25.2.0 or earlier, see Upgrading from 25.2.0 or earlier for a focused checklist and examples.
Boilerplate app
Use the Flutter boilerplate app for a working example of the Atomic SDK for Flutter. You can clone or download it from its GitHub repository.
Installation
The Flutter SDK is available in a public GitHub repository. To include it in your project, add the Git repository link to your pubspec.yaml file and run flutter pub get:
dependencies:
atomic_sdk_flutter:
git:
url: https://github.com/atomic-app/atomic-sdk-flutter-releases.git
ref: 26.1.0
The ref property should be set to the version number of the Flutter SDK that you wish to use. A list of version numbers is documented in our changelog.
Atomic Flutter SDK uses Dart SDK >=3.0.0 <4.0.0.
You also need to make the following changes for each platform:
iOS
Set your iOS deployment target to 16.0 or higher: in Xcode, raise iOS Deployment Target on the Runner project, and if your project has a Podfile, make its platform line read platform :ios, '16.0'.
From 26.1.0 the SDK supports Swift Package Manager, and it is the installation method we recommend. Flutter uses Swift Package Manager for iOS native dependencies by default since Flutter 3.44, and CocoaPods is in maintenance mode. On Flutter 3.44 or later there is nothing to enable. On Flutter 3.24 to 3.43, enable Swift Package Manager once with:
flutter config --enable-swift-package-manager
The Atomic dependency then resolves automatically through the SDK when you build, so there is nothing to add. With Swift Package Manager enabled, do NOT add the pod 'AtomicCards', ... line below, as it would embed the frameworks twice.
CocoaPods
Projects with Swift Package Manager disabled fetch the Atomic dependency with CocoaPods instead. The minimum Flutter version is unchanged either way.
- In the
Podfilein your app'siossubdirectory, ensure the platform line reads:
platform :ios, '16.0'
- At the end of the target block, add the following line to fetch the Atomic dependency:
pod 'AtomicCards', :git => 'https://github.com/atomic-app/action-cards-swiftui-sdk-releases', :tag => "26.2.0"
- In the same directory, run
pod installto fetch the Atomic dependency. If you are unable to run this command, check that you have Cocoapods installed.
Android
The Android SDK is built with Jetpack Compose, so your app module must meet the build
requirements below, and it brings the Jetpack Compose runtime and its androidx
dependencies into your app. Check for conflicts with any Compose or androidx versions
you already use. Nothing needs to be added to your dependencies block: the Flutter SDK
declares the Atomic Android SDK and the Maven repository it comes from.
| Requirement | Minimum | Why |
|---|---|---|
minSdk | 24 | Atomic SDK floor |
compileSdk | 35 | required by the SDK and its AndroidX dependencies |
| Android Gradle plugin | 8.13.2 | earlier versions bundle an R8 that cannot parse the SDK's Kotlin 2.3 metadata (implies Gradle 8.13 and JDK 17) |
| Kotlin Gradle plugin | 2.3.21 | Atomic SDK floor (built with Kotlin 2.3.21) |
| Core-library desugaring | desugar_jdk_libs 2.0.3 | the SDK uses java.time while your minimum SDK is below 26 |
If the Kotlin Gradle plugin is older than the minimum, the build stops during configuration with a message naming the requirement, rather than failing later with a compiler error.
- Set your build requirements. In your
android/app/build.gradlefile:
android {
compileSdk = 35
compileOptions {
coreLibraryDesugaringEnabled = true
}
defaultConfig {
minSdk = 24
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
}
Your Android Gradle plugin and Kotlin Gradle plugin versions are declared in
android/settings.gradle.
- Open your
MainActivityclass inside your app project, and change the base class fromFlutterActivitytoAACFlutterActivity:
import io.atomic.atomic_sdk_flutter.AACFlutterActivity
class MainActivity: AACFlutterActivity() {
...
}
AACFlutterActivity extends FlutterFragmentActivity. The SDK needs this both to host
card views and to attach the modal container and toast messages over your app.
- Open
android/app/src/main/AndroidManifest.xmland add the following attribute to theapplicationtag:
<application ... android:supportsRtl="true">...</application>
This lets the SDK's snooze and feedback screens lay out correctly.
Upgrading from 25.2.0 or earlier
This section explains the upgrade path from version 25.2.0 or earlier to 26.1.0. Version 26.1.0 moves the Flutter SDK to new underlying native SDKs. This changes the build configuration on both platforms and a small part of the Dart API.
Upgrade checklist
- Point the
atomic_sdk_flutterdependencyrefat26.1.0in yourpubspec.yaml(see Installation). - Set your iOS deployment target to 16.0 (see iOS installation).
- Replace the iOS dependency. Prefer Swift Package Manager, or replace the
AtomicSDKpod with theAtomicCardspod (see iOS dependency migration). - Meet the new Android build requirements:
minSdk24,compileSdk35, Android Gradle plugin 8.13.2, Kotlin Gradle plugin 2.3.21, and core-library desugaring (see Android installation). - Restore the Flutter template theme parent in your
styles.xmlfiles (see Android theme migration). - Remove the ProGuard keep rules that earlier versions of this guide asked for (see Android ProGuard migration).
- Replace integer
cardMaxWidthvalues withAACCardMaxWidthvalues (see Card width migration). - Remove reads of
eventNamefrom yourrequestRuntimeVariablesimplementation (see Runtime variables migration). - Move Android reads of
AACSession.notificationFromPushPayloadto the push payload itself, because the method now parses only on iOS (see Push notifications). - Review code that observes cards, derives custom UI from card data, or performs API-driven card actions, because the card model was redesigned (see API-driven card containers).
- Review the configuration properties deprecated in 26.1.0 (see Deprecated configuration properties).
AI-assisted migration
You can use an AI coding assistant or coding agent to help plan and perform this upgrade.
Results depend on the coding model and reasoning mode, available project context, project complexity, app architecture, SDK version, and human review. Treat generated changes as a starting point: your team is responsible for reviewing, testing, and validating the migration.
Click Prompt for AI coding tools to show the full prompt. This prompt has been tried with Claude Fable 5.1 in Claude Code using maximum reasoning on a medium-sized Flutter project, but results will vary by project and coding model.
Prompt for AI coding tools
Upgrade this Flutter project from Atomic Flutter SDK 25.2.0 (or earlier) to 26.1.0.
Official docs:
- Upgrade guide: https://documentation.atomic.io/sdks/flutter#upgrading-from-25-2-0
- Flutter SDK: https://documentation.atomic.io/sdks/flutter
- Changelog: https://documentation.atomic.io/resources/changelog
Goal:
Perform a behavior-preserving upgrade, not just a version bump.
Before coding:
1. Inspect the repo and identify every Atomic touch point:
- the atomic_sdk_flutter dependency in pubspec.yaml
- ios/Podfile (platform line, Atomic source and pod lines) and the iOS deployment target
- android/app/build.gradle (minSdk, compileSdk, desugaring) and android/settings.gradle (Android Gradle plugin and Kotlin Gradle plugin versions)
- android/app/src/main/res/values/styles.xml and values-night/styles.xml theme parents
- android/app/proguard-rules.pro
- AACSession login/auth, session delegate, and JWT handling
- environment IDs, API keys, base URLs, stream/container IDs
- stream containers, single card views, horizontal containers, and their configurations (especially cardMaxWidth and votingOption)
- runtime variable delegates (requestRuntimeVariables and AACCardInstance usage, especially eventName)
- API-driven card container observers and card actions
- counts, filters, custom events, push registration, and badge behavior
2. Provide a concise plan with files likely to change.
3. Call out decisions/risks, especially the iOS dependency manager (Swift Package Manager vs CocoaPods), Android toolchain versions, and any code that reads the old API-driven card model.
4. Wait for approval.
Upgrade requirements:
- Point the atomic_sdk_flutter git dependency ref at 26.1.0.
- Set the iOS deployment target to 16.0.
- On iOS, prefer Swift Package Manager. On Flutter 3.44 or later it is the default. On Flutter 3.24 to 3.43, enable it with `flutter config --enable-swift-package-manager`. With Swift Package Manager enabled, remove the Atomic pod line so the frameworks are not embedded twice.
- If the project stays on CocoaPods, replace the pod 'AtomicSDK' line with:
pod 'AtomicCards', :git => 'https://github.com/atomic-app/action-cards-swiftui-sdk-releases', :tag => "26.2.0"
- Raise Android minSdk to 24 and compileSdk to 35, use Android Gradle plugin 8.13.2 or later and Kotlin Gradle plugin 2.3.21 or later, and enable core-library desugaring with desugar_jdk_libs 2.0.3.
- In styles.xml and values-night/styles.xml, change the LaunchTheme and NormalTheme parent from Theme.MaterialComponents.Light.NoActionBar back to @android:style/Theme.Light.NoTitleBar.
- Remove the Atomic keep rules from proguard-rules.pro (the com.atomic.actioncards, kotlin.coroutines.Continuation, and retrofit2 rules from the old guide).
- Replace integer cardMaxWidth assignments with AACCardMaxWidth values.
- Remove reads of eventName on AACCardInstance and use lifecycleId and the runtime variable names instead.
- Migrate API-driven card container code to the new AACCard model (a flat components list, with lifecycleId, status, and runtimeVariables directly on AACCard).
- Preserve session/login, JWT/auth flow, user identity mapping, IDs/keys/URLs, filters, counts, observers, events, actions, push registration, badge behavior, and user-facing message flows.
Security constraints:
- Do not print, log, summarize, or expose API keys, JWTs, auth tokens, push tokens, or PII.
- If secrets are hardcoded, preserve migration behavior but report them as a security finding.
- Do not change token claims, session delegate behavior, user identity generation, environment selection, or base URLs unless required and approved.
- Do not replace authenticated flows with anonymous/demo/fallback flows to make the upgrade compile.
- Do not weaken TLS, certificate pinning, App Transport Security, notification entitlements, or push environment selection.
- Avoid adding new logs around auth, token fetching, push payloads, card payload metadata, or SDK event raw contents.
Constraints:
- Do not redesign unrelated UI/navigation.
- Do not change Atomic IDs, API keys, base URLs, or token claims unless required by the new SDK.
- Accept SDK-native visual differences where pixel parity with the previous release is impractical.
- Do not add automated tests unless explicitly requested.
- Keep changes scoped to Atomic integration points.
After approval:
1. Implement the upgrade.
2. Resolve dependencies (flutter pub get, and pod install where applicable).
3. Verify the app builds for both platforms, including a minified Android release build if the project uses minification.
4. Report changed files, preserved behavior, build command/result, remaining warnings, manual QA needs, and any security findings.
Manual verification checklist:
- Both platforms build.
- Login/session and JWT token flow work.
- All Atomic containers render.
- Vertical, horizontal, and single card views work.
- Counts and filters update correctly.
- Custom events and card actions work.
- Push permission, device registration, stream registration, and badge updates behave as before.
Build migration
iOS dependency migration
Set your iOS deployment target to 16.0. In Xcode, raise iOS Deployment Target on the Runner project. If your project has a Podfile, make its platform line read platform :ios, '16.0'.
Earlier versions fetched the Atomic dependency with these CocoaPods lines:
# 25.2.0 and earlier
source 'https://github.com/atomic-app/action-cards-ios-sdk-specs.git'
source 'https://github.com/CocoaPods/Specs.git'
pod 'AtomicSDK', :git => 'https://github.com/atomic-app/action-cards-ios-sdk-releases', :tag => "25.2.0"
With Swift Package Manager, which we recommend, remove all of these lines. The Atomic dependency then resolves automatically through the SDK (see iOS installation). Do not keep an Atomic pod line next to Swift Package Manager, as it would embed the frameworks twice.
If you stay on CocoaPods, replace the AtomicSDK line with the new AtomicCards line. The source lines are no longer required.
# 26.1.0
pod 'AtomicCards', :git => 'https://github.com/atomic-app/action-cards-swiftui-sdk-releases', :tag => "26.2.0"
Android theme migration
Earlier versions asked you to set the parent attribute of LaunchTheme and NormalTheme to Theme.MaterialComponents.Light.NoActionBar in android/app/src/main/res/values/styles.xml and values-night/styles.xml. That theme resolved through a Material dependency the SDK no longer ships, so leaving it in place now fails the build with an unresolved resource. Restore the Flutter template's own parent:
<!-- 25.2.0 and earlier -->
<style name="LaunchTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<style name="NormalTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- 26.1.0 -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
Also confirm that your app module meets the new build requirements in Android installation.
Android ProGuard migration
The SDK no longer needs keep rules of its own (see ProGuard configuration). If your proguard-rules.pro contains the rules this guide previously specified, remove them. These rules apply to the previous SDK's classes and its Retrofit dependency. The current SDK does not need them:
-keep class com.atomic.actioncards.** { *; }
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
API migration
Card width migration
AACStreamContainerConfiguration.cardMaxWidth is now an AACCardMaxWidth value rather than an int (see Maximum card width).
// 25.2.0 and earlier
config.cardMaxWidth = 500;
// 26.1.0
config.cardMaxWidth = const AACCardMaxWidth.fixed(500);
The previous default of 0 is now the default AACCardMaxWidth.fill(). Pass alignment: to AACCardMaxWidth.fixed to position the cards within the container.
Runtime variables migration
eventName was removed from AACCardInstance, so a requestRuntimeVariables implementation that reads it fails to compile after upgrading (see Runtime variables). To tell card types apart, inspect lifecycleId and the runtime variable names instead.
// 25.2.0 and earlier
for (final card in cardInstances) {
if (card.eventName == 'order_shipped') {
card.resolveRuntimeVariable('orderNumber', '7296');
}
}
// 26.1.0
for (final card in cardInstances) {
final isOrderCard =
card.runtimeVariables.any((variable) => variable.name == 'orderNumber');
if (isOrderCard) {
card.resolveRuntimeVariable('orderNumber', '7296');
}
}
API-driven card model migration
The API-driven card model was redesigned in 26.1.0. AACCard now exposes a flat, strongly-typed List<AACCardComponent>, and the card's lifecycleId, status, and runtimeVariables are properties directly on AACCard. The defaultView element tree, the instance wrapper, and eventName no longer exist. If your app observes cards, derives custom UI from card data, or performs API-driven card actions, review that code against API-driven card containers and the 26.1.0 entry in the changelog.
Deprecated configuration properties
These properties are deprecated in 26.1.0. They still compile, so the upgrade does not force a change, but remove them when you can:
votingOption: configure the card voting menu in Atomic Workbench instead (see Card voting). The property is ignored on Android.- The
presentationStylevaluewithContextualButton: this value now behaves aswithoutButton. No contextualCloseorBackbutton is displayed, so present the stream container with your own dismissal affordance. automaticallyLoadNextCard: has no effect. The single card view always shows the live feed's first card and advances automatically.enableEdgeToEdgeHandler: has no effect. Window insets are handled automatically on Android.launchColors.statusBarBackground: has no effect. No platform reads this color.
Setup
Before you can display an Atomic stream container or single card view in your app, you must configure the SDK.
You can find your API base URL in the Atomic Workbench, under Configuration > SDK > API Host.
The SDK API base URL is different to the API base URL endpoint, which is also available under Configuration. The SDK API base URL ends with client-api.atomic.io.
You also need to provide the SDK a session delegate for resolving authentications.
Convenient initialization method
You can use a convenient method AACSession.login to initialize the API base URL, environment ID, session delegate, and API key all at once.
It's the equivalent of calling initialise, setSessionDelegate and setApiBaseUrl in sequence, all introduced in the sections below.
The following code snippet shows how to call this method.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.login(
'<environmentId>',
'<apiKey>',
YourSessionDelegate(),
'<url>',
);
On Android you can also pass requestTokenImmediately: true, which asks your session delegate for a token as soon as the session starts rather than when the SDK first needs one. A delegate that cannot supply a token then reports at login instead of at the first card request, at the cost of one token the SDK may not use. It defaults to false, applies only to the session being established, and is ignored on iOS.
SDK API base URL
You can set your API base URL in code, by calling the setApiBaseUrl method:
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setApiBaseUrl('<url>');
Environment ID and API key
Within your host app, you will need to call the initialise method to configure the SDK. Your environment ID can be found in the Atomic Workbench, under Configuration, and your API key can be configured under Configuration > SDK > SDK API keys.
The apiKey value is the Key name you set when adding an SDK API key, not the public key or any generated ID. See SDK API keys for details.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.initialise('<environmentId>', '<apiKey>');
Authenticating requests using a JWT
Atomic SDK uses a JSON Web Token (JWT) to perform authentications.
The SDK Authentication guide provides step-by-step instructions on how to generate a JWT and add a public key to the Workbench.
Within your host app, you will need to call AACSession.setSessionDelegate to provide an object implementing the AACSessionDelegate mixin, which contains only one method: Future<String?> authToken.
It is expected that the token returned by this method represents the same user until you call the logout method.
Define the session delegate
import 'package:atomic_sdk_flutter/atomic_stream_container.dart';
class YourSessionDelegate with AACSessionDelegate {
Future<String?> authToken() async {
<return the token>
}
}
Pass the session delegate
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setSessionDelegate(YourSessionDelegate());
JWT Expiry interval
The Atomic SDK allows you to configure the time interval to determine whether the JSON Web Token (JWT) has expired. If the interval between the current time and the token's exp field is smaller than the seconds you set, the token is considered to be expired.
The interval must not be smaller than zero.
If this method is not called, the default expiry interval is 60 seconds.
On Android the value is accepted and logged but currently has no effect: token expiry is judged from the token's own exp and iat claims there.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setTokenExpiryInterval(120);
JWT Retry interval
The Atomic SDK allows you to configure the timeout interval (in seconds) between retries to get a JSON Web Token from the session delegate if it returns a null token. The SDK will not request a new token sooner than this interval after a failed attempt. The default interval is 0 seconds on iOS and 5 seconds on Android. An interval of 0 means the SDK retries immediately on iOS. On Android, 0 keeps the 5-second default.
For cross-platform consistency, pass whole-second values. Android stores this interval in whole seconds, so any fractional component is discarded.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setTokenRetryInterval(10);
Custom JWT user ID field
By default, the SDK looks for the user ID in the following JWT fields, in order: atomic_sub, atomic_id, sub, id.
If your SDK API key is configured in Workbench to use a different JWT field for the Atomic user ID, call setTokenUserIdAttribute before authenticating or during the login flow. For Workbench configuration details, see Custom ID field.
Treat this as session-scoped configuration. If you call logout() and later authenticate another user who still relies on a custom claim, call this method again.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setTokenUserIdAttribute('custom_user_id');
WebSockets and HTTP API protocols
Atomic SDK uses WebSocket as the default protocol and HTTP as a backup. However, you can switch to HTTP by using AACSession.setApiProtocol, which accepts a parameter of type AACApiProtocol. You can call this method at any time and it will take effect immediately. The setting will last until the host app restarts.
The AACApiProtocol enum has two values:
webSockets: Represent the WebSockets protocol.http: Represent the HTTP protocol.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setApiProtocol(AACApiProtocol.http);
Global error handling
Flutter provides a FlutterError.onError error handler allowing the app to deal with errors in one place. Atomic SDK redirects the errors/exceptions to this pipeline as well. Simply set the handler in your host app. By default, this calls FlutterError.presentError, which dumps the error to the device logs. For more details on Flutter error handling, see Flutter documentation.
FlutterError.onError = (details) {
// You handle errors here.
};
Displaying containers
This section applies to the stream container, single card view, horizontal container view and modal container.
To display an Atomic view in your app, create an instance of AACStreamContainer, AACSingleCardView, AACHorizontalContainerView or AACModalContainerView. To create an instance, you supply:
- (required) A stream container ID, which uniquely identifies the stream container in the app.
- (required) A configuration object, which provides initial styling and presentation information to the SDK for this stream container.
- (optional) Other parameters that support a variety of functionalities.
Stream container ID
First, you’ll need to locate your stream container ID.
Navigate to the Workbench, select Configuration > SDK > Stream containers and find the ID next to the stream container you are integrating.
Configuration options
The configuration object is a class of AACStreamContainerConfiguration, which allows you to configure a stream container or single card view via the following properties:
Style and presentation
-
presentationStyle: (iOS only) indicates how the stream container is being displayed, deciding the button in its top left. It has no effect on Android. Set this using theAACPresentationStyleenum:withoutButton: With no button in its top left.withActionButton: With an action button that triggers a custom action you handle. This value has no effect in horizontal container view.withContextualButton: Deprecated in 26.1.0. This value now behaves aswithoutButton: no contextualCloseorBackbutton is displayed, so present the stream container with your own dismissal affordance. This value has no effect in horizontal container view.
-
launchColors: anAACStreamContainerLaunchColorsobject for customizing colors on first time launch, before a theme has been loaded.background: The background color to use for the launch screen, seen on the first load. Defaults to white.text: The text color to use for the view displayed when the SDK is first presented. Defaults to black at 50% opacity.loadingIndicator: The color to use for the loading spinner on the first time loading screen. Defaults to black.button: The color of the buttons that allow the user to retry the first load if the request fails. Defaults to black.statusBarBackground: Deprecated. No platform reads this color, so it has no effect and will be removed.
-
interfaceStyle: TheAACInterfaceStyleto apply to the stream container. Possible values areautomatic,light, anddark. -
enabledUiElements: A bitmask of UI elements that should be enabled in the stream container. Defaults to showing toast messages, the card list header, and the camera-access request toast in a stream container, and has no effect in single card view. Possible values are:none: No UI elements should be displayed. Do not use it in conjunction with any other values.cardListToast: Toast messages should appear at the bottom of the card list. Toast messages appear when cards are submitted, dismissed or snoozed, or when an error occurs in any of these actions.cardListFooterMessage: (currently iOS only) A footer message should be displayed below the last card in the card list, if at least one is present. The message is customized using theAACCustomString.cardListFooterMessagecustom string. This value has no effect in horizontal container view and single card view.cardListHeader: The header should display at the top of the card list, allowing the user to pull down from the top of the screen to refresh the card list.requestCameraUsage(currently iOS only, introduced in 26.1.0): A toast prompting the user to enable camera access in Settings should be shown when a card needs the camera and permission has been denied. Enabled by default. Remove it fromenabledUiElementsto suppress the prompt.defaultValue: A combination ofcardListToast,cardListHeader, andrequestCameraUsage. Toast messages, the card list header, and the camera-access request toast should be shown.
AACUIElementis a bitmask, so values can be combined with|, intersected with&, inverted with~, and tested withcontains(). For example, to enable toasts, the footer message, and the pull-to-refresh header:configuration.enabledUiElements =
AACUIElement.cardListToast | AACUIElement.cardListFooterMessage | AACUIElement.cardListHeader; -
enableEdgeToEdgeHandler: Deprecated. No platform reads this setting: window insets are handled automatically on Android. It has no effect and will be removed.
Maximum card width
cardMaxWidth: You can specify the width and horizontal alignment of each card within a vertical stream container or a single card view.
It's applicable to both vertical containers and single card views.
cardMaxWidth is now an AACCardMaxWidth value rather than an int. Replace cardMaxWidth = 500 with cardMaxWidth = const AACCardMaxWidth.fixed(500). The previous default of 0 is now the default AACCardMaxWidth.fill().
The default value is AACCardMaxWidth.fill(), which expands each card to fill the container's width while preserving the horizontal padding applied by the SDK. Use AACCardMaxWidth.fixed(width) to constrain cards to a specific width, optionally passing alignment: to position them within the container. The alignment can be AACCardMaxWidthAlignment.leading, .center, or .trailing, and defaults to .center.
To set this, use the cardMaxWidth property in AACStreamContainerConfiguration, and apply this configuration when initializing the stream container.
However, there are a few considerations for using this property:
-
For iOS, it's advised not to set a fixed width of less than
200to avoid layout constraint warnings due to possible insufficient space for the content within the cards. -
A fixed width must be greater than
0. To have cards match the width of their container, useAACCardMaxWidth.fill()rather than a zero width. -
The padding between a card and its container is never smaller than the container padding specified in your theme, which defaults to 10 display points.
-
This property has no effect in horizontal containers, which size their cards using
cardWidthinstead.
The following code snippet sets a fixed card width of 500.
final config = AACStreamContainerConfiguration();
config.cardMaxWidth = const AACCardMaxWidth.fixed(500);
final streamContainer = AACStreamContainer(containerId: "1234", configuration: config)
Functionality
pollingInterval: How frequently the card list should be automatically refreshed. Defaults to 15 seconds, and must be at least 1 second. If set to 0, the card list will not automatically refresh after the initial load.pollingIntervalonly applies to HTTP polling and has no effect when WebSockets is on.
Setting the card refresh interval to a value less than 15 seconds may negatively impact device battery life and is not recommended.
filters: Filters applied to the card list from the moment the container first loads, so an unfiltered feed is never shown. Defaults tonull(no filters). See Filtering cards for how to create filters. To change filters on an already-visible container, useapplyFilterson the state passed toonViewLoaded, which replaces whatever is set here.runtimeVariableResolutionTimeout: The maximum amount of time, in seconds, allocated to the resolution of runtime variables in yourruntimeVariableDelegate'srequestRuntimeVariablesmethod. If you do not return the processed card list before the timeout is reached, the default values for all runtime variables will be used. If you do not implement this delegate method, this property is not used. Defaults to 5 seconds.votingOption(AACVotingOption): Sets the card voting options available from a card's overflow menu. Deprecated in 26.1.0 in favor of the card voting menu configuration in Atomic Workbench. Ignored on Android.both: The user can flag a card as either useful or not useful.notUseful: The user can flag a card as 'not useful' only.useful: The user can flag a card as 'useful' only.none: The user cannot vote on a card (default).
ignoresSafeAreaEdges: A set of screen edges the container may expand under, ignoring the safe area. Defaults to empty, meaning the container does not expand under any edge. The background and chrome extend under the chosen edges while card content stays inside the safe area. Applies to stream containers and modal containers only. iOS only.
Custom strings
The configuration object also allows you to specify custom strings for features in the SDK, using the setValueForCustomString method, which accepts an enumeration AACCustomString and a string value.
The enumeration AACCustomString has such values:
cardListTitle: The title for the card list in this stream container. Defaults to "Cards".cardSnoozeTitle: The title for the feature allowing a user to snooze a card. Defaults to "Remind me".cardDismissTitle(introduced in 26.1.0): The title for the dismiss menu item in the card overflow menu. Defaults to "Dismiss".awaitingFirstCard: The message displayed over the card list, when the user has never received a card before. Defaults to "Cards will appear here when there’s something to action."allCardsCompleted: The message displayed when the user has received at least one card before, and there are no cards to show. Defaults to "All caught up".votingUseful: The title to display for the action a user taps when they flag a card as useful. Defaults to "This is useful".votingNotUseful: The title to display for the action a user taps when they flag a card as not useful. Defaults to "This isn't useful".votingFeedbackTitle: The title to display at the top of the screen allowing a user to provide feedback on why they didn't find a card useful. Defaults to "Send feedback".votingFeedbackValidationMessage(introduced in 26.1.0): The validation message shown when voting feedback reaches the 280 character limit. Defaults to "Feedback is limited to 280 characters".cardListFooterMessage: The message to display below the last card in the card list, provided there is at least one present. Does not apply in horizontal container and single card view, and requiresenabledUiElementsto containAACUIElement.cardListFooterMessage. Defaults to an empty string.noInternetConnectionMessage: The error message shown when the user does not have an internet connection. Defaults to "No internet connection".dataLoadFailedMessage: The error message shown when the theme or card list cannot be loaded due to an API error. Defaults to "Couldn't load data".tryAgainTitle: The title of the button allowing the user to retry the failed request for the card list or theme. Defaults to "Try again".toastCardDismissedMessage: Customized toast message for when the user dismisses a card. Defaults to "Card dismissed".toastCardCompletedMessage: Customized toast message for when the user completes a card. Defaults to "Card completed".toastCardSnoozeMessage: Customized toast messages for when the user snoozes a card. Defaults to "Snoozed until X" where X is the time the user dismissed the card until.toastCardFeedbackMessage: Customized toast message for when the user sends feedback (votes) for a card. Defaults to "Feedback received".processingStateMessage: The message displayed on the upload processing overlay during file upload. Defaults to "Sending, please wait...".processingStateCancelButtonTitle: The text displayed on the cancel button in the upload overlay during file upload. Defaults to "Cancel process".toastFileUploadFailedMessage: (currently iOS only) Customized toast message shown when file(s) fail to upload during card submission. Defaults to "Couldn't upload file(s)".requestCameraAccessMessage: Customized message shown when requesting camera access from the user. Defaults to "Access to your camera is required to take photos. Please enable camera access in your device settings".requestCameraAccessSettingsTitle: The title for the button in the camera access toast, which navigates to the Settings app. Defaults to "Settings".thumbnailImageActionLinkTitle: The call-to-action text displayed at the bottom of a thumbnail image element. Defaults to "View".thumbnailVideoActionLinkTitle: The call-to-action text displayed at the bottom of a thumbnail video element. Defaults to "Watch".
Other parameters
You can also provide other optional parameters when you create a stream container or a single card view:
actionDelegate: An optional delegate that handles actions triggered inside the stream container, such as the tap of the custom action button in the top left of the stream container, or submit and link buttons with custom actions.eventDelegate: An optional delegate that responds to card events in the stream container.runtimeVariableDelegate: An optional runtime variable delegate that resolves runtime variable for the cards.onViewLoaded: An optional callback that allows post-loading actions, such as applying stream container filters.onSizeChanged: An optional callback for single card view and horizontal container view, triggered when the view changes size.
Displaying a stream container
You can now create a stream container by supplying the stream container ID and configuration object on instantiation:
import 'package:atomic_sdk_flutter/atomic_stream_container.dart';
Container(
child: AACStreamContainer(
configuration: <config>,
containerId: '<containerId>',
// Optional parameters
runtimeVariableDelegate: <runtime variable delegate>,
actionDelegate: <action delegate>,
eventDelegate: sdkDelegate,
onViewLoaded: (state) {
print('Container loaded');
},
),
// Specify desired width and height.
width: 400,
height: 400
});
Displaying a single card
The Atomic Flutter SDK also supports rendering a single card in your host app.
To create an instance of AACSingleCardView that is configured in the same way as a stream container, you supply the following parameters on instantiation:
- The ID of the stream container to render in the single card view. The single card view renders only the first card that appears in that stream container.
- A configuration object, which provides initial styling and presentation information to the SDK for the single card view.
The single card view configuration AACSingleCardConfiguration is a subclass of the configuration for a stream container, which inherits most of its options. The only configuration option that does not apply is presentationStyle, as the single card view does not display a header, and therefore does not show a button in its top left.
import 'package:atomic_sdk_flutter/atomic_single_card_view.dart';
AACSingleCardView(
configuration: config,
containerId: '<containerId>',
// Optional parameters
runtimeVariableDelegate: <runtime variable delegate>,
actionDelegate: <action delegate>,
eventDelegate: sdkDelegate,
onViewLoaded: (state) {
print('Container loaded');
},
onSizeChanged: onSizeChanged // Optional - triggered when the single card view changes size.
);
Within a single card view, toast messages (such as those seen when submitting, dismissing or snoozing a card in a stream container) do not appear. Pull to refresh functionality is also disabled.
The single card view automatically sizes itself to fit the card it is displaying. You can also be notified when the single card view changes size, by assigning a callback to the onSizeChanged property on the single card view. You will be supplied with the width and height of the single card view as arguments.
Configuration options for the single card view
There is an extra option in AACSingleCardConfiguration:
automaticallyLoadNextCard: When enabled, will automatically display the next card in the single card view if there is one, using a locally cached card list. Defaults tofalse. Deprecated in 26.1.0: the single card view always shows the live feed's first card and advances automatically, so this property has no effect.
Displaying a horizontal container
The Atomic Flutter SDK supports rendering a horizontal list of cards using AACHorizontalContainerView.
To create a horizontal container, supply a stream container ID and an AACHorizontalContainerConfiguration. The horizontal configuration extends AACStreamContainerConfiguration and adds options specific to horizontal scrolling.
import 'package:atomic_sdk_flutter/atomic_horizontal_container_view.dart';
final config = AACHorizontalContainerConfiguration(cardWidth: 320.0)
..emptyStyle = AACHorizontalContainerConfigurationEmptyStyle.shrink
..headerAlignment = AACHorizontalContainerConfigurationHeaderAlignment.left
..lastCardAlignment = AACHorizontalContainerConfigurationLastCardAlignment.center
..scrollMode = AACHorizontalContainerConfigurationScrollMode.snap;
AACHorizontalContainerView(
configuration: config,
containerId: '<containerId>',
runtimeVariableDelegate: <runtime variable delegate>,
actionDelegate: <action delegate>,
eventDelegate: sdkDelegate,
onViewLoaded: (state) {
print('Horizontal container loaded');
},
onSizeChanged: (width, height) {
print('Horizontal container size changed to $width x $height');
},
);
Configuration options for the horizontal container view
There are extra options in AACHorizontalContainerConfiguration:
cardWidth: The width of every card displayed in the horizontal container view. This value is required.emptyStyle: Determines how the view displays when there are no cards. Usestandardto display the no-card UI, orshrinkto collapse the view. Defaults tostandard.headerAlignment: Controls the horizontal alignment of the header title. Usecenterorleft. Defaults tocenter.lastCardAlignment: Controls the alignment of the last card when there is only one card in the container. Useleft,centerorscaleToFill. WithscaleToFill, the last card scales its width to fill the container, with a default padding, andcardWidthis ignored while only one card remains. Defaults toleft.scrollMode: Controls the scroll mode. Usesnapto scroll one card at a time, orfreefor free scrolling. Defaults tosnap.
Displaying a modal container
The Atomic Flutter SDK can display the first card in a stream container full-screen over your app, using AACModalContainerView.
Unlike the other views, AACModalContainerView wraps your content instead of taking up space in your layout. Its child renders exactly as it would without it. While the widget is in the tree, the SDK displays the container's first card over your whole app whenever the container has cards, and removes it when the last card is actioned or the user logs out.
import 'package:atomic_sdk_flutter/atomic_modal_container_view.dart';
final config = AACModalContainerConfiguration()
..pollingInterval = 30
..position = const AACModalContainerPosition.bottom(24);
AACModalContainerView(
configuration: config,
containerId: '<containerId>',
runtimeVariableDelegate: <runtime variable delegate>,
actionDelegate: <action delegate>,
eventDelegate: sdkDelegate,
onViewLoaded: (state) {
print('Modal container loaded');
},
child: const HomeContent(),
);
Wrap the screens where an interruption is acceptable. Wrapping your whole app lets a card appear anywhere.
Configuration options for the modal container
There is one extra option in AACModalContainerConfiguration:
-
position: The vertical position of the card within the modal container. UseAACModalContainerPosition.center()to center it vertically, orAACModalContainerPosition.top(offset)/AACModalContainerPosition.bottom(offset)to anchor it to an edge with an inset in display points. A negative offset behaves as 0. Defaults tocenter(). A card taller than the available height ignores the position and scrolls from the top.By default the card is positioned inside the safe area, so an offset of 0 sits below a notch or above a home indicator rather than against the screen edge itself. To position against the screen edge instead, add that edge to
ignoresSafeAreaEdgeson the same configuration, and the offset is then measured from the screen edge. iOS only.
Behavior to plan for
- Only the first card is displayed. The rest of the container's cards are displayed one at a time as each is actioned.
- The SDK controls dismissal. There is no method to dismiss the modal container. It is removed when the container has no cards left or the user logs out, so keep the widget mounted for as long as a card should be able to appear.
- Mount only one at a time. If a second
AACModalContainerViewis mounted while another is active, it stays inactive until the first is removed from the tree. - An inactive anchor does not report
onViewLoaded. No platform view is created for it while it is inactive, so the callback does not fire. It fires if and when that anchor becomes the active one, which is worth knowing since applying filters relies ononViewLoadedhaving fired. - The first anchor mounted keeps ownership. Pushing a new screen on top of one that already has an anchor does not hand ownership to the new screen, even though it is now on top, because the route underneath stays mounted.
- The card covers your app. Your own navigation is not reachable while a card is displayed, so do not rely on the user being able to leave the screen.
- Known issue on iOS SDK 26.2.0. If the widget is removed from the tree while a card is displayed, the SDK does not remove the card and it stays on screen. Keep the widget mounted until the container is empty. This will be fixed in a future iOS SDK release.
refresh,updateVariablesandapplyFiltersrecreate the container, which removes and re-displays a visible card. Apply filters fromonViewLoaded.
Closing a stream container
Stream containers, single card views and horizontal container views are dismissed like other views or controllers. There is no specific method that needs to be called. The modal container is the exception: it is dismissed by the SDK, not the host app. See Displaying a modal container for details.
Customizing the first time loading behavior
When a stream container with a given ID is launched for the first time on a user's device, the SDK loads the theme and caches it for future use. On subsequent launches of the same stream container, the cached theme is used and the theme is updated in the background, for the next launch. Note that this first-time loading screen is not presented in single card view and horizontal container view. If those views fail to load, they collapse to a height of 0.
The SDK supports some basic properties to style the first-time load screen, which displays a loading spinner in the center of the container. If the theme or card list fails to load for the first time, an error message is displayed with a 'Try again' button. One of two error messages is possible: 'Couldn't load data' or 'No internet connection'.
First-time loading screen colors are customized using the following properties on the AACStreamContainerLaunchColors object assigned to AACStreamContainerConfiguration.launchColors:
launchColors: anAACStreamContainerLaunchColorsobject for customizing colors on first time launch, before a theme has been loaded.background: The background color to use for the launch screen, seen on the first load. Defaults to white.text: The text color to use for the view displayed when the SDK is first presented. Defaults to black at 50% opacity.loadingIndicator: The color to use for the loading spinner on the first time loading screen. Defaults to black.button: The color of the buttons that allow the user to retry the first load if the request fails. Defaults to black.statusBarBackground: Deprecated. No platform reads this color, so it has no effect and will be removed.
You can also customize the text for the first load screen error messages and the 'Try again' button, using the setValueForCustomString method of AACStreamContainerConfiguration.
Note: These customized error messages also apply to the card list screen.
AACCustomString.noInternetConnectionMessage: The error message shown when the user does not have an internet connection. Defaults to "No internet connection".AACCustomString.dataLoadFailedMessage: The error message shown when the theme or card list cannot be loaded due to an API error. Defaults to "Couldn't load data".AACCustomString.tryAgainTitle: The title of the button allowing the user to retry the failed request for the card list or theme. Defaults to "Try again".
API-driven card containers
You can observe stream containers through the pure SDK API, even when that container's UI is not loaded into memory.
The API-driven card model was redesigned in 26.1.0. AACCard now exposes a flat, strongly-typed List<AACCardComponent>, and the card's lifecycleId, status, and runtimeVariables are properties directly on AACCard. The examples in this section use the 26.1.0 model. Note that this API-driven card interface is an unstable, MVP-stage API without a stability guarantee.
When you opt to observe a stream container, it is updated by default immediately after any changes in the published cards. Should the WebSocket be unavailable, the cards are updated at regular intervals, which you can specify. Upon any change in cards, the handler block is executed with the updated card list or null if the cards couldn't be fetched. Note that the specified time interval for updates cannot be less than 1 second.
The following code snippet shows the simplest use case scenario:
await AACSession.observeStreamContainer(
containerId: streamContainerId,
callback: (cards) {
if (cards == null) {
print("The cards could not be loaded.");
}
else {
print("There are ${cards.length} cards in the container.");
}
},
);
This method returns a token that you can use to stop the observation, see Stopping the observation for more details.
In the callback, the cards parameter is an array of AACCard objects. Each AACCard contains a variety of other class types that represent the card elements defined in Workbench. Detailed documentation for the classes involved in constructing an AACCard object is not included in this guide. However, you can refer to the examples provided below, which demonstrate several typical use cases.
Configuration options
The method accepts an optional configuration parameter. The configuration object, AACStreamContainerObserverConfiguration, allows you to customize the observer's behavior with the following properties, which are all optional:
- pollingInterval: defines how frequently the system checks for updates when the WebSocket service is unavailable. The default interval is 15 seconds, but it must be at least 1 second. If a value less than 1 second is specified, it defaults to 1 second.
- filters: filters applied when fetching cards for the stream container. It defaults to
null, meaning no filters are applied. See Filtering cards for more details of stream filtering.
The legacy filter AACCardFilter.byCardInstanceId for observeStreamContainer only works on iOS, not Android.
- runtimeVariables: A map of runtime variables which will be resolved before observing the stream container. Defaults to
null. See Runtime variables for more details of runtime variables. - runtimeVariableResolutionTimeout: the maximum time allocated for resolving variables in the delegate. If the tasks within the delegate method exceed this timeout, or if the completionHandler is not called within this timeframe, default values will be used for all runtime variables. The default timeout is 5 seconds and it cannot be negative.
- runtimeVariableAnalytics: whether the
runtime-vars-updatedanalytics event, which includes the resolved values of each runtime variable, should be sent upon resolution. The default setting isfalse. If you set this flag totrue, ensure that the resolved values of your runtime variables do not contain sensitive information that shouldn't appear in analytics. See SDK analytics for more details on runtime variable analytics.
Stopping the observation
The observer ceases to function when you call AACSession.logout(). Alternatively, you can stop the observation using the token returned from the observation call mentioned above:
// Start observing and save the observer's token.
final token = await AACSession.observeStreamContainer(
containerId: streamContainerId,
callback: (_) {
print("observeStreamContainer test");
},
);
// Stop the observer using the previously saved token.
await AACSession.stopObservingStreamContainer(token);
Examples
Accessing card metadata
Card metadata encompasses data that, while not part of the card's content, are still critical pieces of information. Key metadata elements include:
- Card instance ID: This is the unique identifier assigned to a card upon its publication.
- Card priority: Defined in the Workbench, this determines the card's position within the feed. The priority will be an integer between 1 & 10, a priority of 1 indicates the highest priority, placing the card at the top of the feed.
- Action flags: Also defined in the Workbench, these flags dictate the visibility of options such as dismissing, snoozing, and voting menus for the card.
The code snippet below shows how to access these metadata elements for a card instance.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.observeStreamContainer(
containerId: "1",
callback: (cards) {
final card = cards?.first;
if (card != null) {
print("The card instance ID is ${card.id}");
print("The priority of the card is ${card.metadata.priority}");
print("The card has a dismiss option in its overflow menu: ${card.actions.dismiss.overflow ? "yes" : "no"}.");
}
},
);
Reading card components
Components represent the contents that are defined in the Workbench on the Content page of a card. The components property of the AACCard exposes the card's default view as a flat, ordered list of typed AACCardComponent objects. Each component class matches one element type from the Workbench, such as AACHeadline, AACTextBlock, AACCategory, AACMedia, AACButtonContainer and the input components.
The code snippet below shows how to read the components of a card and extract the text representing the card's category.
import 'package:atomic_sdk_flutter/atomic_data_interface.dart';
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.observeStreamContainer(
containerId: "1",
callback: (cards) {
final card = cards?.first;
if (card != null) {
for (final component in card.components) {
if (component is AACCategory) {
final categoryText =
component.textItems.map((item) => item.text).join(", ");
print("The card's category is: $categoryText");
}
}
}
},
);
The card model returned to the callback is an AACCard. The id property is the card's instance ID, lifecycleId is the lifecycle ID sent with the event that created the card, and status is the card's current status. The runtimeVariables property lists all runtime variables in use by the card, with resolved values applied. The components property holds the card's default view, subviews is a Map<String, AACCardSubview> where each subview has a title and its own components, metadata is an AACCardMetadata, and actions is an AACCardActions. Metadata includes receivedAt, priority, lastCardActiveTime and createdDate. Action flags include dismiss, snooze, voteUp and voteDown. The dismiss and snooze actions carry overflow and swipe flags, and the voting actions carry an overflow flag alongside feedback settings.
Accessing subviews
Subviews are layouts that differ from the card's default view, and each has a unique subview ID. See Link to subview on how to get the subview ID.
The following code snippet shows how to retrieve a subview layout using a specific subview ID, which you can find in the workbench:
await AACSession.observeStreamContainer(
containerId: "1",
callback: (cards) {
final card = cards?.first;
final subview = card?.subviews["<subview ID>"];
if (subview != null) {
print("Accessing subview ${subview.title}");
final subviewComponents = subview.components;
// Do something with subviewComponents.
}
},
);
Or traverse all subview layouts for that card:
await AACSession.observeStreamContainer(
containerId: "1",
callback: (cards) {
final card = cards?.first;
card?.subviews.forEach((subviewId, subview) {
print("Accessing subview ${subview.title}");
final subviewComponents = subview.components;
// Do something with subviewComponents.
});
},
);
Dynamically displaying a card's components
The componentWidget method below is an example that maps some AACCardComponent subclasses to simple Text widgets. The cardColumn method uses it to display a whole card with a Column widget.
import 'package:atomic_sdk_flutter/atomic_data_interface.dart';
import 'package:flutter/material.dart';
Widget componentWidget(AACCardComponent component) {
if (component is AACHeadline) {
return Text(
component.text,
style: const TextStyle(fontWeight: FontWeight.bold),
);
}
if (component is AACTextBlock) {
return Text(component.text);
}
if (component is AACListItem) {
return Text(component.text);
}
if (component is AACButtonContainer) {
return Text(
"Buttons: ${component.buttons.map((button) => button.text).join(", ")}",
);
}
return Text("Other component: ${component.runtimeType}");
}
Column cardColumn(AACCard card) {
return Column(
children: [
for (final component in card.components) componentWidget(component),
],
);
}
API-driven card actions
You can execute card actions through the pure SDK API. The currently supported actions are: dismiss, submit, and snooze. These use the AACCardActionType values Dismiss, Submit, and Snooze. To execute these card actions, follow these three steps:
- Create a card action object: Use the corresponding initialization methods of the
AACCardActionclass. These returnAACDismissCardAction,AACSubmitCardAction, orAACSnoozeCardAction. You'll need a container id and a card instance ID for this. The card instance ID can be obtained from anAACCardfromAACSession.observeStreamContainer(see API-driven card containers for more details). - Execute the action: Call the method
AACSession.executeCardActionto perform the card action. - Check the result of the action in the result callback: The result will be an
AACCardActionResultenum.
Dismissing a card
The following code snippet shows how to dismiss a card.
await AACSession.executeCardAction(
containerId,
cardId,
AACCardAction.dismiss(),
(result) {
switch (result) {
case AACCardActionResult.Success:
print("Card $cardId dismissed!");
break;
case AACCardActionResult.DataError:
print("Card $cardId DataError!");
break;
// iOS only: Android currently reports DataError for every failed action.
case AACCardActionResult.NetworkError:
print("Card $cardId NetworkError!");
break;
}
},
);
Submitting a Card
You have the option to submit certain values along with a card. These values are optional and should be encapsulated in a Map<String, dynamic> object, using String keys and values that are either Strings, numbers, or bools.
While editing cards in Workbench, you can add input components onto cards and apply various validation rules, such as Required, Minimum length, or Maximum length. The input elements can be used to submit user-input values, where the validation rules are applied when submitting cards through UIs of stream containers.
However, for this non-UI version, support for input components is not available yet. There is currently no mechanism to store values in these input components through this API, and the specified validation rules won't be enforced when submitting cards.
Atomic cards include button names when they are submitted. The button name will be added to analytics to enable referencing the triggering button in an Action Flow. Therefore, you need to provide a button name when submitting cards.
Getting the button name
The button name of a submit button can be acquired when receiving cards through API-driven cards. You can also find the button name on the button element in the workbench. The following code snippet shows how to obtain button name of the first submit button from the top-level of the first card.
String? _buttonName;
final token = await AACSession.observeStreamContainer(
containerId: "1",
callback: (cards) {
if (cards != null) {
// Traverse the components from the top-level of the first card.
for (final component in cards.first.components) {
if (component is AACButtonContainer) {
for (final button in component.buttons) {
if (button.action case final AACButtonActionSubmit submit) {
// Save the first submit button's name and stop looking.
_buttonName = submit.buttonName;
return;
}
}
}
}
}
},
);
Submitting the card
With button name obtained, you can now submit the card. The following code snippet shows how to submit a card with specific values.
// Obtain _buttonName
...
const submittedValues = <String, Object>{
"stringKey": "string",
"numberKey": 22,
"booleanKey": false,
};
if (_buttonName != null) {
await AACSession.executeCardAction(
containerId,
cardId,
AACCardAction.submit(_buttonName, submittedValues),
(result) {
switch (result) {
case AACCardActionResult.Success:
print("Card $cardId submitted with values!");
break;
case AACCardActionResult.DataError:
print("Card $cardId DataError!");
break;
// iOS only: Android currently reports DataError for every failed action.
case AACCardActionResult.NetworkError:
print("Card $cardId NetworkError!");
break;
}
},
);
}
Snoozing a Card
When snoozing a card, you must specify a non-negative interval in seconds. Otherwise an error will be returned.
The following code snippet shows how to snooze a card for a duration of 1 minute.
const snoozeInterval = 60;
await AACSession.executeCardAction(
containerId,
cardId,
AACCardAction.snooze(snoozeInterval),
(result) {
switch (result) {
case AACCardActionResult.Success:
print("Card $cardId snoozed for $snoozeInterval seconds!");
break;
case AACCardActionResult.DataError:
print("Card $cardId DataError!");
break;
// iOS only: Android currently reports DataError for every failed action.
case AACCardActionResult.NetworkError:
print("Card $cardId NetworkError!");
break;
}
},
);
Dark mode
Stream containers in the Atomic Flutter SDK support dark mode. You configure an (optional) dark theme for your stream container in the Atomic Workbench.
The interface style determines which theme is rendered:
automatic: If the user's device is currently set to light mode, the stream container will use the light (default) theme. If the user's device is currently set to dark mode, the stream container will use the dark theme (or fallback to the light theme if this has not been configured). On iOS versions less than 13, this setting is equivalent tolight.light: The stream container will always render in light mode, regardless of the device setting.dark: The stream container will always render in dark mode, regardless of the device setting.
Filtering cards
Stream containers (vertical or horizontal), single card views and container card count observers can have one or more filters applied. These filters determine which cards are displayed, or how many cards are counted.
A stream container filter consists of two parts: a filter value and an operator.
Filter values
The filter value is used to filter cards in a stream container. The following list outlines all card attributes that can be used as a filter value.
| Card attribute | Description | Value type |
|---|---|---|
| Priority | Card priority defined in Workbench, Card -> Delivery | int |
| Card template created date | The date time when a card template is created | DateTime |
| Card template ID | The template ID of a card, see below for how to get it | String |
| Card template name | The template name of a card | String |
| Custom variable | Action Flow variables, optionally referenced in the card content | Multiple |
Use the corresponding named constructors of AACCardFilterValue to create a filter value.
Examples
Card priority
The following code snippet shows how to create a filter value that represents a card priority 4.
final filterValue = AACCardFilterValue.byPriority(4);
Card template ID
The following code snippet shows how to create a filter value that represents a card template ID.
final filterValue = AACCardFilterValue.byCardTemplateId("templateId");
Custom variable
The following code snippet shows how to create a filter value that represents a boolean custom variable isSpecial.
final filterValue = AACCardFilterValue.byVariableNameBool("isSpecial", false);
Note: It's important to specify the right value type when referencing custom variables for filter value. There are five types of variables in the Workbench, currently four are supported:
- String:
AACCardFilterValue.byVariableNameString(String variableName, String value) - Number:
AACCardFilterValue.byVariableNameInt(String variableName, int value), orAACCardFilterValue.byVariableNameDouble(String variableName, double value)when the value has a fractional part - Date:
AACCardFilterValue.byVariableNameDateTime(String variableName, DateTime value) - Boolean:
AACCardFilterValue.byVariableNameBool(String variableName, bool value)
On the card editing page, click on the ID part of the overflow menu at the upper-right corner.

Filter operators
The operator is the operational logic applied to a filter value (some operators require 2 or more values).
The following table outlines available operators.
| Operator | Description | Supported types |
|---|---|---|
| equalTo | Equal to the filter value | int, double, DateTime, String, bool |
| notEqualTo | Not equal to the filter value | int, double, DateTime, String, bool |
| greaterThan | Greater than the filter value | int, double, DateTime |
| greaterThanOrEqualTo | Greater than or equal to the filter value | int, double, DateTime |
| lessThan | Less than the filter value | int, double, DateTime |
| lessThanOrEqualTo | Less than or equal to the filter value | int, double, DateTime |
| contains | Matches any of the filter values | int, double, DateTime, String |
| notIn | Not in one of the filter values | int, double, DateTime, String |
| between | In the range of start and end, inclusive | int, double, DateTime |
After creating a filter value, use the corresponding named constructor on the AACCardFilter class to combine it with an operator.
On Android, the between operator does not currently support AACCardFilterValue.byCreatedDate. The SDK ignores the filter and writes a warning to the log.
Examples
Card priority range
The following code snippet shows how to create a filter that filters card with priority between 2 and 6 inclusive.
final filterValue1 = AACCardFilterValue.byPriority(2);
final filterValue2 = AACCardFilterValue.byPriority(6);
final filter = AACCardFilter.between(filterValue1, filterValue2);
Each operator supports different type of values. For example, operator lessThan only supports numeric and Date values. So passing String values to that operator will raise an exception.
Applying filters to a stream container or a card count observer
There are three steps to filter cards in a stream container or for a card count observer:
-
Create one or more
AACCardFilterValueobjects. -
Combine filter values with filter operators to form a
AACCardFilter. -
Apply filter(s).
3.1. For stream containers, set the
filtersproperty on the container's configuration to apply filters from the first load, so an unfiltered feed is never shown. To change filters on an already-visible container, use theonViewLoadedcallback, where the container objectstateis provided. Filters applied there replace the configuration's.- To apply a single filter, call
await state.applyFilter(filter). - To apply multiple filters, call
await state.applyFilters(List<AACCardFilter>? filters). EachapplyFiltercall overrides the previous call (not incremental). So if you want to apply multiple filters at the same time, use theapplyFilters(List<AACCardFilter>? filters)method rather than multipleapplyFilter(filter)methods. - To delete all existing filters, pass either
nullor an empty list[]to theapplyFiltersmethod, ornullto theapplyFiltermethod.
import 'package:atomic_sdk_flutter/atomic_stream_container.dart';
AACStreamContainer(
configuration: ...,
containerId: '1234',
onViewLoaded: (state) async {
final filter = AACCardFilter.byCardInstanceId('cardId1234');
await state.applyFilter(filter);
// or, for multiple filters: await state.applyFilters(filters);
// or, to delete all filters: await state.applyFilters([]);
},
),3.2. For card count observers, pass a
Listof filters to parameterfilterswhen creating an observer using theAACSession.observeCardCountmethod. - To apply a single filter, call
Examples
Card priority 5 and above
The following code snippet shows how to only display cards with priority > 5 in a stream container.
...
onViewLoaded: (state) async {
final filterValue = AACCardFilterValue.byPriority(5);
final filter = AACCardFilter.greaterThan(filterValue);
// Acquire the stream container object and apply filter
await state.applyFilter(filter);
}
Earlier than a set date
The following code snippet shows how to only display cards created earlier than 9/Jan/2023 inclusive in a stream container.
...
onViewLoaded: (state) async {
final filterValue = AACCardFilterValue.byCreatedDate(DateTime(2023, 1, 9));
final filter = AACCardFilter.lessThanOrEqualTo(filterValue);
await state.applyFilter(filter);
}
Card template names
The following code snippet shows how to only display cards with the template names 'card 1', 'card 2', or 'card 3' in a stream container.
...
onViewLoaded: (state) async {
final filterValue1 = AACCardFilterValue.byCardTemplateName("card1");
final filterValue2 = AACCardFilterValue.byCardTemplateName("card2");
final filterValue3 = AACCardFilterValue.byCardTemplateName("card3");
final filter = AACCardFilter.contains([filterValue1, filterValue2, filterValue3]);
await state.applyFilter(filter);
}
Combination of filter values
The following code snippet shows how to only display cards with priority != 6 and custom variable isSpecial == true in a stream container.
Note: isSpecial is a Boolean custom variable defined in Workbench.
...
onViewLoaded: (state) async {
final filterValue1 = AACCardFilterValue.byPriority(6);
final filter1 = AACCardFilter.notEqualTo(filterValue1);
final filterValue2 = AACCardFilterValue.byVariableNameBool("isSpecial", true);
final filter2 = AACCardFilter.equalTo(filterValue2);
await state.applyFilters([filter1, filter2]);
}
Legacy filter
The legacy filter, AACCardFilter.byCardInstanceId(String cardInstanceId), is still supported. This filter requests that the stream container or single card view show only a card matching the specified card instance ID, if it exists. An instance of this filter can be created using the corresponding named constructor on the AACCardFilter class.
The card instance ID can be found in the push notification payload, allowing you to apply the filter in response to a push notification being tapped.
...
onViewLoaded: (state) async {
final filter = AACCardFilter.byCardInstanceId("ABCD-1234");
await state.applyFilter(filter);
}
The legacy filter AACCardFilter.byCardInstanceId for the observeStreamContainer method only works on iOS, not Android.
Also, the legacy filter doesn't work for observeCardCount on both platforms.
Nonetheless, it does work on both platforms for the applyFilters method.
Removing all filters
-
For stream containers, pass
nullor an empty list[]to theapplyFilters(List<AACCardFilter>? filters)method. -
For stream container observers,
filtersis an optional parameter (set tonullby default). The filters cannot be changed after creating the observer:
await AACSession.observeStreamContainer(
containerId: '<containerId>',
config: AACStreamContainerObserverConfiguration(filters: myFilters),
callback: (cards) {
// Handle the observed cards.
},
);
Supporting custom actions on submit and link buttons
In the Atomic Workbench, you can create a submit or link button with a custom action payload.
- When such a link button is tapped, the
didTapLinkButtonmethod is called on your action delegate. - When such a submit button is tapped, and after the card is successfully submitted, the
didTapSubmitButtonmethod is called on your action delegate.
The parameter to each of these methods is an action object, containing the payload that was defined in the Workbench for that button. You can use this payload to determine the action to take, within your app, when the submit or link button is tapped.
The action object also contains the card instance ID and stream container ID where the custom action was triggered.
On iOS, the action object also carries buttonName, the name of the tapped button as configured in the Atomic Workbench, and source (AACCardActionSource), the card element the action came from: a submit button, a link button, or an image. Both are currently null on Android.
import 'package:atomic_sdk_flutter/atomic_stream_container.dart';
// 1. Extend the action delegate.
class MyActionDelegate with AACStreamContainerActionDelegate {
void didTapActionButton() {
print("The action button was tapped.");
}
void didTapLinkButton(AACCardCustomAction action) {
print("The link button was clicked, card id: ${action.cardInstanceId}");
print("The container id is ${action.containerId}");
print("The action payload is ${action.actionPayload}");
}
void didTapSubmitButton(AACCardCustomAction action) {
print("The submit button was clicked, card id: ${action.cardInstanceId}");
print("The container id is ${action.containerId}");
print("The action payload is ${action.actionPayload}");
}
}
// 2. Assign an event delegate on instantiation.
...
AACStreamContainer(
configuration: <config>,
containerId: <container ID>,
actionDelegate: myActionDelegate,
)
Card snoozing
The Atomic SDKs provide the ability to snooze a card from a stream container or single card view. Snooze functionality is exposed through the card’s action buttons, overflow menu and the quick actions menu (exposed by swiping a card to the left, on iOS and Android).
Tapping on the snooze option from either location brings up the snooze date and time selection screen. The user selects a date and time in the future until which the card will be snoozed. Snoozing a card will result in the card disappearing from the user’s card list or single card view, and reappearing again at the selected date and time. A user can snooze a card more than once.
When a card comes out of a snoozed state, if the card has an associated push notification, and the user has push notifications enabled, the user will see another notification, where the title is prefixed with Snoozed:.
You can customize the title of the snooze functionality, as displayed in a card’s overflow menu and in the title of the card snooze screen. The default title, if none is specified, is Remind me.
On the AACStreamContainerConfiguration object, call the setValueForCustomString method to customize the title for the card snooze functionality:
configuration.setValueForCustomString(AACCustomString.cardSnoozeTitle, 'Snooze');
Card voting
The Atomic SDKs support card voting, which allows you to gauge user sentiment towards the cards you send. When integrating the SDKs, you can choose to enable options for customers to indicate whether a card was useful to the user or not, accessible when they tap on the overflow button in the top right of a card.
If the user indicates that the card was useful, a corresponding analytics event is sent for that card (card-voted-up).
If they indicate that the card was not useful, they are presented with a secondary screen where they can choose to provide further feedback. The available reasons for why a card wasn’t useful are:
- It’s not relevant.
- I see this too often.
- Something else.
If they select "Something else", a free-form input is presented, where the user can provide additional feedback. The free form input is limited to 280 characters. After tapping "Submit", an analytics event containing this feedback is sent (card-voted-down).
You can customize the titles that are displayed for these actions, as well as the title displayed on the secondary feedback screen. By default these are:
- Thumbs up: "This is useful".
- Thumbs down: "This isn’t useful".
- Secondary screen title: "Send feedback".
Card voting is disabled by default. Configure which voting options are available in the card voting menu in Atomic Workbench.
votingOption propertyThe votingOption property on the stream container configuration is the legacy way to enable voting. The property is deprecated in 26.1.0, and it only has an effect on iOS. Android ignores it. Do not use it in new integrations.
You can also customize the titles for the card voting options, and the title displayed at the top of the feedback screen, presented when a user indicates the card wasn’t useful:
configuration.setValueForCustomString(AACCustomString.votingFeedbackTitle, 'Provide feedback');
configuration.setValueForCustomString(AACCustomString.votingUseful, 'Thumbs up');
configuration.setValueForCustomString(AACCustomString.votingNotUseful, 'Thumbs down');
Refreshing a stream container manually
You can choose to manually refresh a stream container or single card view, such as when a push notification arrives while your app is open. Refreshing results in the stream container or single card view checking for new cards immediately, and showing any that are available.
Note On Flutter the stream container is a stateful widget, whose view is actually controlled by its AACViewState, so you need to call refresh on the state object provided in onViewLoaded.
await viewState.refresh();
Responding to card events
The SDK allows you to perform custom actions in response to events occurring on a card, such as when a user:
- submits a card.
- dismisses a card.
- snoozes a card.
- indicates a card is useful (when card voting is enabled).
- indicates a card is not useful (when card voting is enabled).
- fails to submit, dismiss, or snooze a card.
To be notified when these happen, assign a card event delegate to your stream container:
The AACCardEvent.kind property is one of these AACCardEventKind values:
submitteddismissedsnoozedvotedUsefulvotedNotUsefulsubmitFaileddismissFailedsnoozeFailedcameraDenied(currently iOS only): the user has denied the app permission to use the camera, which a card needscameraRestricted(currently iOS only): device policy restricts the app from using the camera
Beyond its kind, an AACCardEvent carries the cardInstanceId of the card the event relates to, and buttonName, the name of the submit button that submitted the card as configured in the Atomic Workbench (populated for submitted events). Either is null when the platform does not report it.
// 1. Extend the event delegate.
class MyEventDelegate with AACCardEventDelegate {
void didTriggerCardEvent(AACCardEvent event) {
// Perform a custom action in response to the card event.
print('The event ${event.kind.stringValue} happened in the stream container.');
}
}
...
// 2. Assign an event delegate on instantiation.
AACStreamContainer(
configuration: <config>,
containerId: <container ID>,
eventDelegate: myEventDelegate,
);
Sending custom events
You can send custom events directly to the Atomic Platform for the logged in user, via this static method in AACSession:
Future<void> sendCustomEvent(
String eventName, {
Map<String, Object>? eventProperties,
});
The eventProperties parameter is optional. Property values may be strings, numbers or booleans, and are sent with their type preserved.
A custom event can be used in the Workbench to create segments for card targeting. For more details of custom events, see Custom Events.
The event will be created for the user defined by the authentication token returned in the session delegate (which is registered when initiating the SDK). As such, you cannot specify target user IDs using this method.
const eventName = "myEvent";
final properties = {
"firstName": "John",
"lastName": "Smith",
};
await AACSession.sendCustomEvent(eventName, eventProperties: properties);
Error handling
The sendCustomEvent method may throw an error if unsuccessful, so it is recommended to wrap it in a try/catch statement:
try {
await AACSession.sendCustomEvent(eventName, eventProperties: properties);
} catch (error) {
// handle the error
print("Sending custom event failed $error");
}
API and additional methods
Push notifications
To use push notifications in the Flutter SDK, configure a notification platform for your app in the Workbench (see: Notifications), then request push notification permission in your app. For iOS, the Workbench takes an APNs push certificate or an APNs authentication token. For Android, it takes the Firebase service account private key (JSON) for FCM HTTP v1.
Obtain the push token
Atomic delivers iOS notifications directly through the Apple Push Notification service (APNs), and Android notifications through Firebase Cloud Messaging (FCM). The Flutter SDK does not obtain push tokens itself. Your push plugin obtains the token, and you pass it to the SDK as a string. The token is different on each platform:
- On iOS, pass the hexadecimal string of the APNs device token. The SDK decodes this string into the raw APNs token before it registers the device, so an FCM registration token does not work on iOS.
- On Android, pass the FCM registration token.
Any push plugin that gives you these tokens works with the SDK, and so does your own platform channel that forwards the token from the native push callbacks. The examples in this section use firebase_messaging for convenience, because most Flutter apps already depend on it for FCM on Android. Its getAPNSToken() returns the APNs token on iOS, and getToken() returns the FCM token on Android.
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
final messaging = FirebaseMessaging.instance;
final pushToken = Platform.isIOS
? await messaging.getAPNSToken()
: await messaging.getToken();
On iOS, getAPNSToken() returns null until APNs has issued a token to the device. Wait for the token before you register it, and listen to your plugin's token refresh stream so that you can send a new token to Atomic when it changes.
Android notification channels
On Android 8 and above, FCM can only display a notification on a notification channel your app has created. Create a channel at app start, then either enter its id as the Channel Id in the Workbench notification settings for your Android app, or declare it as FCM's default channel in your AndroidManifest.xml:
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="your_channel_id" />
If the channel named in an incoming push does not exist, Android shows the notification on a Firebase fallback channel named "Miscellaneous", with default importance and none of your channel settings.
Notifications received in the foreground
Neither platform shows a notification for a message that arrives while your app is in the foreground unless your app asks for it. On Android, FCM delivers the message to your plugin's foreground handler instead of showing it. To show it, create a local notification with a package such as awesome_notifications or flutter_local_notifications. On iOS, the app delegate decides whether a foreground notification is shown. With firebase_messaging, call setForegroundNotificationPresentationOptions to opt in.
Register with the Atomic Platform
Once your app receives a push token, register it with the Flutter SDK. Steps 1 and 2 below can occur in either order. Repeat both steps each time the logged in user changes, so the Atomic Platform knows which user the device belongs to.
1. Register the user against specific stream containers for push notifications
You need to signal to the Atomic Platform which stream containers are eligible to receive push notifications in your app for the current device.
The optional notificationsEnabled parameter updates the user's notificationsEnabled preference in the Atomic Platform. You can also inspect and update this preference using the Atomic API. Consult the API documentation for user preferences for more information.
If you pass false, the user does not receive notifications on any eligible device, even if the device is registered in this step and its push token is sent in the next step. If you pass true, which is the default, the user receives notifications. This lets you enable or disable notifications for the current user from your own app, such as from a notification settings screen.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.registerStreamContainersForNotifications(
['<containerId>'],
notificationsEnabled: false,
);
2. Send the push token to the Atomic Platform
Send the device's push token to the Atomic Platform when it changes. Call this method in the appropriate push notification callback in your app:
import "package:atomic_sdk_flutter/atomic_session.dart";
await AACSession.registerDeviceForNotifications('token');
You can call the registerDeviceForNotifications method any time you want to update the push token stored for the user in the Atomic Platform. Pass the token described in Obtain the push token as a string.
On iOS, registerDeviceForNotifications and deregisterDeviceForNotifications also accept an optional environment, which selects the APNs environment the registration targets. Choose the value that matches the notification platforms configured for your app in the Workbench. The parameter is ignored on Android.
AACPushNotificationEnvironment.sandbox: registers the token against the APNs sandbox environment, which matches theiOS Sandboxplatform in the Workbench. Registration fails with an error if that platform is not configured.AACPushNotificationEnvironment.production: registers the token against the APNs production environment, which matches theiOSplatform in the Workbench. Registration fails with an error if that platform is not configured.AACPushNotificationEnvironment.both, the default: registers against both environments. If only one of them is configured in the Workbench, that one is registered and the missing one does not cause an error. Deregistration behaves the same way.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.registerDeviceForNotifications(
'token',
environment: AACPushNotificationEnvironment.sandbox,
);
To deregister the device for Atomic notifications for your app, such as when a user completely logs out of your app, call deregisterDeviceForNotifications on AACSession:
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.deregisterDeviceForNotifications();
3. Parse and track Atomic push notifications
The methods in this step take the notification payload as a Map<String, dynamic>. The payload differs by platform:
- On iOS, pass the notification's
userInfodictionary. Withfirebase_messaging,message.datacarries the Atomic keys. - On Android, pass the data map of the FCM message, such as
message.datawithfirebase_messaging. Every value in the map must be a string. The SDK throws if you pass the whole message object or a map with values of other types.
On iOS, you can use AACSession.notificationFromPushPayload to determine whether a push payload was sent by the Atomic Platform. If it is an Atomic push notification, the method returns an AACPushNotification object containing the stream container ID, card instance ID and detail payload. Otherwise, it returns null. The returned AACPushNotification also surfaces the rest of the parsed notification, including atomicPushId, endUserId, organisationId and streamContainerIds.
import 'package:atomic_sdk_flutter/atomic_session.dart';
final notification = await AACSession.notificationFromPushPayload(payload);
if (notification != null) {
print('Container ID: ${notification.containerId}');
print('Card instance ID: ${notification.cardInstanceId}');
print('Detail payload: ${notification.detail}');
}
On Android, notificationFromPushPayload always returns null. Versions up to 25.2.0 parsed the payload on Android too, so move any Android reads to the payload itself when you upgrade. To identify an Atomic push notification on Android, read the atomic entry of the data payload that your push plugin delivers, for example message.data with firebase_messaging. The entry is only present on Atomic push notifications. Its value is a JSON string with the streamContainerId, cardInstanceId and detail fields.
import 'dart:convert';
final atomicPayload = message.data['atomic'] as String?;
if (atomicPayload != null) {
final atomic = jsonDecode(atomicPayload) as Map<String, dynamic>;
print('Container ID: ${atomic['streamContainerId']}');
print('Card instance ID: ${atomic['cardInstanceId']}');
print('Detail payload: ${atomic['detail']}');
}
When your app receives an Atomic push notification, call trackPushNotificationReceived with the payload. This works on both platforms, and dispatches an analytics event back to Atomic indicating that the user's device received the notification.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.trackPushNotificationReceived(payload);
The method throws if the SDK has not been initialized or if the payload does not represent an Atomic push notification.
Requesting card count
It is recommended that you use user metrics to retrieve the card count instead. See Retrieving the count of active and unseen cards for more information.
The SDK supports requesting the current card count for a particular stream container with requestCardCount. This is a one-off request and does not continue observing for changes.
import 'package:atomic_sdk_flutter/atomic_session.dart';
final count = await AACSession.requestCardCount('<containerId>');
print('Card count is $count');
The method throws if the count is unavailable, such as when the user does not have access or the internet connection is unavailable.
Observing card count
It is recommended that you use user metrics to retrieve the card count instead. See the next section for more information.
The SDK supports observing the card count for a particular stream container. Card count is provided to your callback independently of whether a stream container or single card view has been created, and is updated at the provided interval.
Future<String> observeCardCount({
required String containerId,
required void Function(int cardCount) callback,
Duration pollingInterval = const Duration(seconds: 1),
List<AACCardFilter>? filters,
});
The pollingInterval must be at least 1 second, otherwise it defaults to 1 second. You can also optionally provide a list of AACCardFilters to the observer. The method returns a String observer token to distinguish card count observers.
The legacy filter AACCardFilter.byCardInstanceId for observeCardCount does not work for both Android and iOS.
If you choose to observe the card count, by default it is updated immediately after the published card number changes. If for some reason the WebSocket is not available, the count is then updated periodically at the interval you specify. The time interval cannot be smaller than 1 second.
When you want to stop observing the card count, you can remove the observer using the token returned from the observation call:
await AACSession.stopObservingCardCount(observerToken);
Full example:
import 'package:atomic_sdk_flutter/atomic_session.dart';
// Retain this token so that you can stop observing later.
String observerToken = await AACSession.observeCardCount(
containerId: '<containerId>',
pollingInterval: const Duration(seconds: 5),
callback: (count) {
print("Card count is now ${count}");
},
// This filter will make the callback only give the count of cards with a priority of 3.
filters: [AACCardFilter.equalTo(AACCardFilterValue.byPriority(3))],
});
// Stop observing for that token
await AACSession.stopObservingCardCount(observerToken);
Retrieving the count of active and unseen cards
All cards are unseen the moment they are sent. A card becomes "seen" when it has been shown on the customer's screen (even if only briefly or partly). A quick scroll-through might not make the card "seen", this depends on the scrolling speed. The user metrics only count "active" cards, which means that snoozed and embargoed cards will not be included in the count.
The Atomic Flutter SDK exposes a new object: AACUserMetrics. These metrics include:
- The number of cards available to the user across all stream containers.
- The number of cards that haven't been seen across all stream containers.
- The number of cards available to the user in a specific stream container (equivalent to the card count functionality in the previous section).
- The number of cards not yet seen by the user in a specific stream container.
These metrics enable you to display badges in your UI that indicate how many cards are available to the user but not yet viewed, or the total number of cards available to the user.
import 'package:atomic_sdk_flutter/atomic_session.dart';
// User metrics across all stream containers.
final metrics = await AACSession.userMetrics('');
print('Total cards across all containers: ${metrics.totalCards}');
print('Unseen cards across all containers: ${metrics.unseenCards}');
// User metrics of a specific stream container.
final containerMetrics = await AACSession.userMetrics('container-1234');
print("Total cards across a specific container: ${containerMetrics.totalCards}");
print("Unseen cards across a specific container: ${containerMetrics.unseenCards}");
Runtime variables
Runtime variables are resolved in the SDK at runtime, rather than from an event payload when the card is assembled. Runtime variables are defined in the Atomic Workbench.
The SDK will ask the host app to resolve runtime variables when a list of cards is loaded (and at least one card has a runtime variable), or when new cards become available due to WebSockets pushing or HTTP polling (and at least one card has a runtime variable).
Runtime variables are resolved by your app via the requestRuntimeVariables method on AACRuntimeVariableDelegate. If you do not implement this method, runtime variables will fall back to their default values, as defined in the Atomic Workbench. To resolve runtime variables, you pass an object implementing the AACRuntimeVariableDelegate mixin to the parameter runtimeVariableDelegate when creating a stream container or a single card view.
Runtime variables can currently only be resolved to string values.
The requestRuntimeVariables method, when called by the SDK, provides you with:
- A list of objects representing the cards in the list. Each card object contains:
- The lifecycle identifier associated with the card.
- A method that you call to resolve each variable on that card (
resolveRuntimeVariable). - A list of
AACCardRuntimeVariablevalues in use by this card. Each runtime variable has anameanddefaultValue.
In 26.1.0, eventName was removed from AACCardInstance, so an implementation that reads it fails to compile after upgrading. To tell card types apart, inspect lifecycleId and the runtime variable names instead.
The method expects a nullable list of resolved cards returned, once all variables are resolved.
If a variable is not resolved, that variable will use its default value, as defined in the Atomic Workbench.
If you do not return the card list before the runtimeVariableResolutionTimeout elapses (defined on AACStreamContainerConfiguration), the default values for all runtime variables will be used.
import 'package:atomic_sdk_flutter/atomic_card_runtime_variable.dart';
import 'package:atomic_sdk_flutter/atomic_stream_container.dart';
class MyCardRuntimeVariableDelegate with AACRuntimeVariableDelegate {
Future<List<AACCardInstance>?> requestRuntimeVariables(
List<AACCardInstance> cardInstances,
) async {
for (AACCardInstance card in cardInstances) {
// Resolve a runtime variable 'numberOfItems' to '12' on all cards.
// You can also inspect `lifecycleId` and `runtimeVariables`
// to determine what type of card this is.
card.resolveRuntimeVariable("numberOfItems", '12');
}
return cardInstances;
}
}
Updating runtime variables manually
You can manually update runtime variables at any time by calling the updateVariables method on the AACViewState object provided in onViewLoaded:
await viewState.updateVariables();
Accessibility and fonts
The Atomic SDKs support a variety of accessibility features on each platform. These features make it easier for vision-impaired customers to use Atomic's SDKs inside of your app.
These features also allow your app, with Atomic integrated, to continue to fulfil your wider accessibility requirements.
Dynamic font scaling
The Atomic Flutter SDK supports dynamic font scaling. Font scaling behave differently on platforms. On iOS this feature is called Dynamic Type. On Android, this feature is enabled in the Settings app, under "Font size".
For more details on dynamic font scaling, see the iOS Dynamic Type or Android Dynamic font scaling documentation.
Using embedded fonts in themes
When creating your stream container's theme in the Atomic Workbench, you optionally define custom fonts that can be used by the stream container for UI elements. When defined in the Workbench, these fonts must point to a remote URL, so that the SDK can download the font, register it against the system and use it.
It is likely that the custom fonts you wish to use are already part of your app, particularly if they are a critical component of your brand identity. If this is the case, you can have a stream container font (with a given font family name, weight and style) reference a font embedded in your app instead. This is also useful if the license for your font only permits you to embed the font and not download it from a remote URL.
To map a font in a stream container theme to one embedded in your app, first add the font file to the project, making sure you also declare the font in the pubspec file. Then use the registerEmbeddedFonts method on AACSession, passing an array of AACEmbeddedFont objects, each containing the following:
- A
familyNamethat matches the font family name declared in the Atomic Workbench. - A
weight, which is a value ofAACFontWeight, also matching the value declared in the Atomic Workbench. - A
style, which is eitherAACFontStyle.italicorAACFontStyle.normal. - A
postscriptName(iOS only), which matches the Postscript name of a font available to your app. This can be a font bundled with your application or one provided by the operating system. - A
typefacePath(Android only), which is the path to the font file inside your app package. For a font declared in yourpubspecfile, this is its asset path prefixed withflutter_assets/, such asflutter_assets/assets/fonts/HelveticaNeue.ttf. A font supplying no typeface path is not registered on Android.
Available AACFontWeight values are regular, bold, weight100, weight200, weight300, weight400, weight500, weight600, weight700, weight800, weight900, and weight950.
If the familyName, weight and style of a font in the stream container theme matches an AACEmbeddedFont instance that you've registered with the SDK, the SDK will use the postscriptName on iOS, or the typefacePath on Android, to create an instance of your embedded font, and will not download the font from a remote URL.
Registered fonts currently cannot be unregistered on Android, so passing an empty list has no effect there.
In the example below, any use of a custom font named BrandFont in your theme, that is bold and italicized, would use the embedded font named HelveticaNeue instead:
If the Postscript name provided is invalid, or the family name, weight and style do not match a custom font in the stream container theme exactly, the SDK will download the font at the remote URL specified in the theme instead.
import 'package:atomic_sdk_flutter/atomic_embedded_font.dart';
import 'package:atomic_sdk_flutter/atomic_session.dart';
final embeddedFont = AACEmbeddedFont(
"BrandFont",
"HelveticaNeue",
AACFontStyle.italic,
AACFontWeight.regular,
typefacePath: "flutter_assets/assets/fonts/HelveticaNeue.ttf",
);
await AACSession.registerEmbeddedFonts([embeddedFont]);
SDK Analytics
The default behavior is to not send analytics for resolved runtime variables. Therefore, you must explicitly enable this feature to use it.
If you use runtime variables on a card, you can optionally choose to send the resolved values of any runtime variables back to the Atomic Platform as an analytics event. This per-card analytics event, runtime-vars-updated, contains the values of runtime variables rendered in the card and seen by the end user. Therefore, you should not enable this feature if your runtime variables contain sensitive data that you do not wish to store on the Atomic Platform.
To enable this feature, set the runtimeVariableAnalytics flag on your configuration's features object:
import 'package:atomic_sdk_flutter/atomic_stream_container.dart';
final config = AACStreamContainerConfiguration();
config.features = AACFeatureFlags()..runtimeVariableAnalytics = true;
Utility methods
Client app version
You can set the current version of your host app for SDK analytics. This makes it easier to segment analytics and investigate issues by app version.
If you do not call this method, the client app version defaults to unknown.
Strings longer than 128 characters are trimmed to that length.
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setClientAppVersion('1.2.3');
Debug logging
Debug logging allows you to view more verbose logs regarding events that happen in the SDK. It is turned off by default, and should not be enabled in release builds. To enable debug logging:
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.enableDebugMode(level);
The parameter level is an integer that indicates the verbosity level of the logs exposed:
- Level 0: Default, no logs exposed.
- Level 1: Operations and transactions are exposed.
- Level 2: Operations, transactions and their details are exposed, plus level 1.
- Level 3: Expose all logs.
Purge cached data
The SDK provides a method for purging the local cache of a user's card data. This method intends to clear user data when a previous user logs out, so that the cache is clear when a new user logs in to your app. This method also sends any pending analytics events back to the Atomic Platform. On iOS, an analytics upload that fails during logout can surface as an error from this call. On Android it currently cannot, since the call errors only when the logout cannot start at all.
Pass deregisterNotifications: true to also deregister this device from Atomic push notifications as part of logging out, so notifications stop arriving for the user who signed out. Without it, the device token outlives the session and notifications keep being delivered. It defaults to false, and a deregistration failure does not fail the logout and is not reported.
To clear this in-memory cache, call:
import 'package:atomic_sdk_flutter/atomic_session.dart';
// Logout example:
await AACSession.logout();
// Logout and also deregister this device from Atomic push notifications:
await AACSession.logout(deregisterNotifications: true);
// Here's an alternative example that handles any logout errors:
try {
await AACSession.logout();
// Code that executes after successfully logging out.
} catch (error) {
// Handle the error.
}
Network request security
The Atomic Flutter SDK provides functionality to further secure the SDK implementation in your app.
Allow or deny network requests
You can set a request policy, which determines whether requests originating from the Atomic SDK are allowed to proceed. This enables you to permit network requests only to domains or subdomains that you approve.
The policy is consulted before every network request in the SDK, and during SSL certificate validation.
To enable this functionality, create an AACRequestPolicy and pass it to AACSession.setRequestPolicy. The policy holds an ordered list of AACRequestRules and a fallback disposition for requests no rule matches. A rule applies to requests whose host is, or is a subdomain of, its hostSuffix: a rule for atomic.io matches atomic.io and api.atomic.io, but not notatomic.io. The first matching rule decides the request, so order specific hosts ahead of broader ones. The fallback defaults to allow.
A disposition is one of the following:
AACRequestDisposition.allow(): The request is allowed to proceed.AACRequestDisposition.deny(): The request is not allowed to proceed and is canceled.AACRequestDisposition.allowWithPins(pins): The request can proceed if a hash of the subject public key info (SPKI) from part of the certificate chain matches one of the pin objects provided (see below for more information).
The SDK consults the policy at the moment each request is made, so set it before the requests you intend to govern: once during app startup, ahead of AACSession.initialise. The policy is not tied to a session and survives logout. Pass null to remove a previously set policy.
If you do not set a request policy, all requests are permitted.
import 'package:atomic_sdk_flutter/atomic_request_policy.dart';
import 'package:atomic_sdk_flutter/atomic_session.dart';
await AACSession.setRequestPolicy(
const AACRequestPolicy(
rules: [
// Allow requests to atomic.io and its subdomains, with certificate pinning.
AACRequestRule(
hostSuffix: 'atomic.io',
disposition: AACRequestDisposition.allowWithPins({
AACCertificatePin('AAAAAA='),
}),
),
// Always allow requests to placeholder.com.
AACRequestRule(
hostSuffix: 'placeholder.com',
disposition: AACRequestDisposition.allow(),
),
],
// Deny all other requests.
fallback: AACRequestDisposition.deny(),
),
);
Allow requests with certificate pinning
When a rule resolves to allowWithPins, you supply a set of AACCertificatePin objects. Each object contains a SHA-256 hash of the subject public key info (SPKI) from part of the domain's certificate chain, which is then base64 encoded. If the hashed, base64-encoded SPKI from part of the request's certificate chain matches any of the provided pins, the request is allowed to proceed. If it does not match any of the provided pins, the request is denied. The convenience constructor AACRequestAllowWithPins.hashes([...]) builds the pin set from raw hash strings.
The hash must be base64, not the hex digest certificate tooling prints by default. A hex digest decodes as base64 without error but to the wrong length, and one unusable pin invalidates the whole set, denying every request the rule applies to. An empty pin set is treated as deny for the same reason: a configuration mistake fails closed rather than silently dropping pinning. Extract the correct value from a certificate with:
openssl x509 -in cert.pem -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| openssl enc -base64
When pinning requests for the Atomic API (<orgId>.client-api.atomic.io), we strongly recommend pinning to all available Amazon root certificates, which is the approach recommended by Amazon Web Services. SPKI SHA-256 hashes for Amazon's root certificates are available on the Amazon Trust website, in hexadecimal form. The SDK needs the base64 form of the same hash, so decode the hexadecimal value to bytes before you encode it. You can convert a hash with the following Terminal command: echo "<SHA256 hash>" | xxd -r -p | openssl base64
There are platform-specific limits on pinned requests:
- (iOS) If
allowWithPinsapplies to a non-HTTPS URL, the request is denied. - (iOS) The WebSockets URL follows a pattern like
wss://[url]/socket. A pinned rule covering it denies the WebSockets connection, and the SDK falls back to HTTP polling. - (iOS) Pinned video playback requires an HTTPS media URL and does not support HLS (
.m3u8) streams, so a pinned rule that covers your media host breaks HLS video on cards there. If you serve HLS, scope your rules so the media host falls outside them, or give that hostAACRequestDisposition.allow(). Android does not have this restriction.
Updating user data
The SDK allows you to update the user profile and preferences on the Atomic Platform for the logged-in user via the updateUser method of AACSession. This user is identified by the authentication token provided by the session delegate that is registered when initiating the SDK.
Setting up profile fields
For simple setup, create an AACUserSettings object and set some profile fields, then call method AACSession.updateUser(userSettings).
The following optional profile fields can be supplied to update the data for the user. A user setting object is equivalent to those settings in the Customers page on the Workbench.
externalID: An optional string that represents an external identifier for the user.name: An optional string that represents the name of the user.email: An optional string that represents the email address of the user.phone: An optional string that represents the phone number of the user.city: An optional string that represents the city of the user.country: An optional string that represents the country of the user.region: An optional string that represents the region of the user.
Any fields which have not been supplied will remain unmodified after the user update.
The following code snippet shows how to set up some profile fields:
final userSettings = AACUserSettings()
..externalID = 'Flutter shell app ID'
..name = 'Flutter user'
..email = 'user@flutter.com'
..phone = '+(64)123456'
..city = 'Flutter city'
..country = 'Flutter country'
..region = 'Flutter region';
await AACSession.updateUser(userSettings);
Setting up custom profile fields
You can also setup your custom fields of the user profile. Custom fields must first be created in Atomic Workbench before updating them. For more details of custom fields, see Custom Fields.
There are two types of custom fields: date and text.
userSettings.setDateForCustomField(DateTime dateTime, String customField)for custom fields defined as type 'date' in the Atomic Workbench.userSettings.setTextForCustomField(String text, String customField)for custom fields defined as type 'text' in the Atomic Workbench.
Note: Use the name property in the Workbench to identify a custom field, not the label property.
The following code snippet shows how to set up a date and a text field:
final userSettings = AACUserSettings()
..setTextForCustomField("Flutter!!", "fluttertext")
..setDateForCustomField(DateTime.now(), "flutterdate");
Setting up notification preferences
You can use the following optional property and method to update the notification preferences for the user. Again, any fields which have not been supplied will remain unmodified after the user update.
notificationsEnabled: An optional Boolean property onAACUserSettings(set directly, e.g.userSettings.notificationsEnabled = false) that sets whether push notifications are enabled for this user. Defaults totrue.setNotificationTime(List<AACUserNotificationTimeframe> timeframes, AACUserNotificationTimeframeWeekdays weekday): An optional method that defines the notification time preferences of the user for different days of the week. If you specifyAACUserNotificationTimeframeWeekdays.anyDayto the second parameter, the notification time preferences will be applied to every day.
Available weekday values are anyDay, monday, tuesday, wednesday, thursday, friday, saturday, and sunday.
Each day accepts a list of notification time periods. These are periods during which notifications are allowed. If an empty array is provided, notifications will be disabled for that day.
The following code snippet shows how to set up notification periods from 8am to 5:30pm and from 7pm to 10pm on Monday:
AACUserSettings().setNotificationTime(
[
AACUserNotificationTimeframe(
startHour: 8,
startMinute: 0,
endHour: 17,
endMinute: 30,
),
AACUserNotificationTimeframe(
startHour: 19,
startMinute: 0,
endHour: 22,
endMinute: 0,
),
],
AACUserNotificationTimeframeWeekdays.monday,
);
Hours are in the 24h format that must be between 0 & 23 inclusive, while minutes are values between 0 & 59 inclusive.
Validating the settings
You can optionally check the settings before submitting them, by calling validate on the AACUserSettings object. It returns null when the settings are valid, or a message describing the problem. AACSession.updateUser performs the same validation and reports failures through the error it throws, so validate is for surfacing a problem in your own UI before submitting. On Android it currently always returns null, and the settings are validated on submission only.
UpdateUser method: full example
The following code snippet shows an example of using the updateUser method to update profile fields, custom profile fields and notification preferences.
final userSettings = AACUserSettings()
..externalID = 'Flutter shell app ID'
..name = 'Flutter user'
..email = 'user@flutter.com'
..phone = '+(64)123456'
..city = 'Flutter city'
..country = 'Flutter country'
..region = 'Flutter region'
..setTextForCustomField("Flutter!!", "fluttertext")
..setDateForCustomField(DateTime.now(), "flutterdate")
..setNotificationTime(
[
AACUserNotificationTimeframe(
startHour: 0,
startMinute: 0,
endHour: 18,
endMinute: 59,
),
AACUserNotificationTimeframe(
startHour: 19,
startMinute: 0,
endHour: 23,
endMinute: 59,
),
],
AACUserNotificationTimeframeWeekdays.anyDay,
);
await AACSession.updateUser(userSettings);
Though all fields of AACUserSettings are optional, you must supply at least one field when calling AACSession.updateUser.
Observing SDK events
The Atomic Flutter SDK provides functionality to observe SDK events that symbolize identifiable SDK activities such as card feed changes or user interactions with cards. The following code snippet shows how to observe SDK events.
await AACSession.setSDKEventObserver((AACSDKEvent sdkEvent) {
// do something with the sdkEvent
});
Only one SDK event observer can be active at a time. If you call this method again, it will replace the previous observer. To remove the SDK event observer, set it to null.
The SDK provides all observed events in the base class AACSDKEvent. Each event shares common information such as eventType, timestamp, identifier, and where appropriate, userId and containerId. On Android, userId is currently populated only for AACSDKEventType.NotificationReceived.
An event has a corresponding eventType property taken from the AACSDKEventType enum. The properties for each event are different and dependent on the eventType, but they closely follow Atomic analytics events. The properties that are not applicable to the event's eventType are set to null. For detailed information about these events, please refer to Analytics reference.
| AACSDKEventType | Analytics | Description |
|---|---|---|
Dismissed | YES | The user dismisses a card. |
Snoozed | YES | The user snoozes a card. |
Submitted | YES | The user submits a card. |
CardFeedUpdated | NO | A card feed has been updated. It occurs when a card(s) has been removed or added to the feed, or the card(s) in the feed has been updated. |
CardDisplayed | YES | A card is displayed in a container. This event monitors the following situations: - User scrolling (tracked once scrolling settles). - Initial load of the card list. - Arrival of new cards that is visible. |
CardVotedUp | YES | The user taps on the "this is useful" option in the card overflow menu. |
CardVotedDown | YES | The user taps the "Submit" button on the card feedback screen, which is brought up by tapping on the "This isn't useful" option in the card overflow menu. |
RuntimeVarsUpdated | YES | A card containing runtime variables has one or more runtime variables resolved. This event occurs on a per-card basis. |
StreamDisplayed | YES | A stream container is first loaded or returned to. |
UserRedirected | YES | The user is redirected by a URL or a custom payload. This happens if they open a URL on a link button, open a URL after submitting a card, or tap on a link or submit button with a custom action payload. This event can occur on either the top-level or subview of a card. |
SnoozeOptionsDisplayed | YES | The snooze date/time selection UI is displayed. |
SnoozeOptionsCanceled | YES | The user taps the "Cancel" button in the snooze UI. |
CardSubviewDisplayed | YES | A subview of card is opened. |
CardSubviewExited | YES | The user leaves the subview, either by navigating back or submitting the card. |
VideoPlayed | YES | The user hits the play button of a video. This event can occur on either the top-level or subview of a card. |
VideoCompleted | YES | A video finishes playing. This event can occur on either the top-level or subview of a card. |
SdkInitialized | YES | An instance of the SDK is initialized, or the JWT is refreshed. |
RequestFailed | YES | Any API request to the Atomic client API fails within the SDK, or a failure in WebSocket causes a fallback to HTTP polling. Note: Network failure and request timeout does not trigger this event. |
NotificationReceived | YES | A push notification is received by the SDK. |
UserFileUploadsStarted | YES | The user began uploading one or more files attached to a card. (currently iOS only) |
UserFileUploadsFailed | YES | One or more of the user's file uploads failed, either due to an API error or lack of network connectivity. (currently iOS only) |
UserFileUploadsCompleted | YES | All of the user's file uploads for a card completed successfully. (currently iOS only) |
UnknownEvent | NO | An event type that is not recognized by this SDK version. |
Some SDK event fields are grouped into model classes. cardContext is an AACSDKEventCardContext, streamContext is an AACSDKEventStreamContext, and properties is an AACSDKEventProperties.
AACSDKEventProperties exposes the following fields, each set only for the event types where it applies:
| Field | Type | Description |
|---|---|---|
subviewId | String? | The unique ID of the subview the event relates to. |
subviewTitle | String? | The title of the subview the event relates to. |
subviewLevel | int? | The depth of the subview (currently 1), or 0 if the event occurred at the top level. |
linkMethod | AACSDKEventLinkMethod? | How the user was redirected (Payload, Url, UnknownLinkMethod). |
detail | AACSDKEventDetail? | The card component that triggered the redirection (Image, LinkButton, SubmitButton, TextLink, UnknownDetail). |
url | String? | The URL the user was redirected to, or for VideoPlayed/VideoCompleted events the URL of the video. |
redirectPayload | Map<String, dynamic>? | The custom action payload used to redirect the user. |
submittedValues | Map<String, dynamic>? | All input values for a Submitted event. |
resolvedVariables | Map<String, String>? | The values used for all runtime variables. If a variable was not resolved by the host app, its default value is reported here. |
reason | AACSDKEventReason? | The selected reason on a CardVotedDown event: TooOften, Other, Relevant (meaning "not relevant"), or UnknownReason. |
message | String? | The free-form feedback text the user provided on a CardVotedDown with reason Other. |
source | String? | The action source, e.g. "Dismiss invoked", "Snooze invoked", "Submit invoked". |
path | String? | The endpoint path at which a RequestFailed event occurred. |
statusCode | int? | The status code returned by the failed endpoint. 0 indicates this RequestFailed event represents a fallback from WebSockets to HTTP polling rather than an HTTP error. |
unsnoozeDate | DateTime? | The date and time at which a snoozed card is scheduled to reappear. |
snoozePeriod | Map<String, int>? | How long the card was snoozed for, as calendar components keyed by unit name (for example day, hour, minute). Only the units the snooze was expressed in are present. On Android every snooze is currently expressed in seconds, so the map holds a single second entry there. |
payloadMetadata | Map<String, dynamic>? | Arbitrary metadata supplied with the card's payload, on a CardDisplayed event. null when the card carried none. (currently iOS only) |
fileInfo | List<AACSDKEventFileUploadInfo>? | The files involved in a file-upload event, each with a filename and the bucketId it was uploaded to. (currently iOS only) |
streamContainerIds | List<String>? | Every stream container the event applies to, on a NotificationReceived event or a RequestFailed event spanning more than one container. The single-container case is carried by containerId on the event itself. |
atomicPushId | String? | The Atomic Platform's identifier for the received push notification. (currently iOS only) |
organisationId | String? | The identifier of the organisation the received push notification belongs to. (currently iOS only) |
sig | String? | The signature accompanying the received push notification. (currently iOS only) |
ia | num? | An internal attribute of the received push notification, passed through unchanged. (currently iOS only) |
notificationDetail | Map<String, dynamic>? | The full payload of the received push notification. (currently iOS only) |
Some SDK event properties expose additional enums. AACSDKEventCardViewState can be TopView, SubView, or UnknownCardViewState. AACSDKEventDisplayMode can be Vertical, Horizontal, Single, Modal (currently Android only, reported when the event comes from a modal container), or UnknownDisplayMode. AACSDKEventLinkMethod can be Payload, Url, or UnknownLinkMethod.
Observing SDK Events examples
An example for logging every SDK event and their properties.
void logSdkEventsCallback(AACSDKEvent sdkEvent) {
final eventString =
"${_getTimeMsg(sdkEvent.timestamp)}\neventType.name: ${sdkEvent.eventType.name},"
"\nidentifier: ${sdkEvent.identifier},\nuserId: ${sdkEvent.userId},\ncardCount: ${sdkEvent.cardCount},"
"\ncardContext: ${sdkEvent.cardContext == null ? "null" : "{"
"${newLineTab}cardInstanceId: ${sdkEvent.cardContext!.cardInstanceId},"
"${newLineTab}cardInstanceStatus: ${sdkEvent.cardContext!.cardInstanceStatus},"
"${newLineTab}cardPresentation: ${sdkEvent.cardContext!.cardPresentation},"
"${newLineTab}cardViewState.name: ${sdkEvent.cardContext!.cardViewState?.name}\n}"},"
"\nproperties: ${sdkEvent.properties == null ? "null" : "{"
"${newLineTab}subviewId: ${sdkEvent.properties!.subviewId},"
"${newLineTab}subviewTitle: ${sdkEvent.properties!.subviewTitle},"
"${newLineTab}subviewLevel: ${sdkEvent.properties!.subviewLevel},"
"${newLineTab}linkMethod.name: ${sdkEvent.properties!.linkMethod?.name},"
"${newLineTab}detail.name: ${sdkEvent.properties!.detail?.name},"
"${newLineTab}url: ${sdkEvent.properties!.url},"
"${newLineTab}submittedValues: ${sdkEvent.properties!.submittedValues},"
"${newLineTab}redirectPayload: ${sdkEvent.properties!.redirectPayload},"
"${newLineTab}resolvedVariables: ${sdkEvent.properties!.resolvedVariables},"
"${newLineTab}reason.name: ${sdkEvent.properties!.reason?.name},"
"${newLineTab}message: ${sdkEvent.properties!.message},"
"${newLineTab}source: ${sdkEvent.properties!.source},"
"${newLineTab}path: ${sdkEvent.properties!.path},"
"${newLineTab}unsnoozeDate: ${sdkEvent.properties!.unsnoozeDate?.toIso8601String()},"
"${newLineTab}statusCode: ${sdkEvent.properties!.statusCode}\n}"},"
"\ncontainerId: ${sdkEvent.containerId},\nstreamContext: ${sdkEvent.streamContext == null ? "null" : "{"
"${newLineTab}streamLength: ${sdkEvent.streamContext!.streamLength},"
"${newLineTab}cardPositionInStream: ${sdkEvent.streamContext!.cardPositionInStream},"
"${newLineTab}streamLengthVisible: ${sdkEvent.streamContext!.streamLengthVisible},"
"${newLineTab}displayMode.name: ${sdkEvent.streamContext!.displayMode?.name}\n}"}";
_allEvents.add(eventString);
}
// Start observing sdk events
await AACSession.setSDKEventObserver(logSdkEventsCallback);
// Stop observing sdk events
await AACSession.setSDKEventObserver(null);
Example output for the CardDisplayed event, using the above callback:
[2024-01-30 08:07:00.000Z]
eventType.name: CardDisplayed,
identifier: <identifier will be here>,
userId: <userId will be here>,
cardCount: null,
cardContext: {
cardInstanceId: <cardInstanceId will be here>,
cardInstanceStatus: active,
cardPresentation: individual,
cardViewState.name: TopView
},
properties: null,
containerId: 123ID,
streamContext: {
streamLength: 12,
cardPositionInStream: 1,
streamLengthVisible: 1,
displayMode.name: Single
}
An example for fetching unseen card number in realtime
When your application will display the number of unseen cards on the app icon, it is crucial to ensure that this number stays current as the user navigates through cards. This way, when they return to the home screen, they see an up to date count of unseen cards. To make this possible, we must fetch the count of unseen cards in real time.
You can obtain the count of unseen cards from user metrics. However, since this is a singular call, we need to invoke this method repeatedly to keep the count current. By monitoring SDK events, we can update the unseen card count every time a card's viewed status changes. The code snippet below shows how to fetch the number of unseen cards for a container under these conditions.
import 'dart:async';
const containerId = "<containerId>";
Future<void> updateUnseenCardCount() async {
final metrics = await AACSession.userMetrics(containerId);
print("Total cards across a specific container: ${metrics.totalCards}");
print("Unseen cards across a specific container:${metrics.unseenCards}");
}
await AACSession.setSDKEventObserver((AACSDKEvent sdkEvent) {
final eventContainerId = sdkEvent.containerId;
final shouldRefresh =
sdkEvent.eventType == AACSDKEventType.CardFeedUpdated ||
sdkEvent.eventType == AACSDKEventType.CardDisplayed;
if (shouldRefresh && eventContainerId == containerId) {
unawaited(updateUnseenCardCount());
}
});
An example for capturing the voting-down event
The following code snippet shows how to capture an event when the user votes down for a card.
await AACSession.setSDKEventObserver((AACSDKEvent sdkEvent) {
if (sdkEvent.eventType == AACSDKEventType.CardVotedDown) {
print("The user has voted down for the card ${sdkEvent.cardContext?.cardInstanceId}");
switch (sdkEvent.properties?.reason) {
case AACSDKEventReason.TooOften:
print("The reason is it's displayed too often.");
case AACSDKEventReason.Relevant:
print("The reason is it's not relevant.");
case AACSDKEventReason.Other:
print("The user provided some other reasons: ${sdkEvent.properties?.message}");
case AACSDKEventReason.UnknownReason:
case null:
print("The reason is unknown.");
}
}
});
Image linking to a URL
You can use images for navigation purposes, such as directing to a web page, opening a subview, or sending a custom payload into the app, as if they were buttons. This functionality is accessible in Workbench, where you can assign custom actions to images on your cards.
The updated analytics event 'user-redirected'
Redirection initiated by images also trigger the user-redirected analytics event. To accurately identify the origin of this event, a new detail property has been added, with four distinct values:
- Image: The event was activated by an image.
- LinkButton: A link button was the source of the event.
- SubmitButton: The redirection was initiated via a submit button.
- TextLink: The trigger was a link embedded within markdown text.
See Analytics or Analytics reference for more details of the event user-redirected.
In the Flutter SDK, you can also capture the detail property via SDK event observer. The following code snippet shows how to parse this property.
import 'package:atomic_sdk_flutter/atomic_session.dart';
import 'package:atomic_sdk_flutter/atomic_sdk_event.dart';
await AACSession.setSDKEventObserver((AACSDKEvent sdkEvent) {
if (sdkEvent.eventType == AACSDKEventType.UserRedirected) {
switch (sdkEvent.properties?.detail) {
case AACSDKEventDetail.Image:
print("Event triggered by an image.");
case AACSDKEventDetail.LinkButton:
print("Event triggered by a link button.");
case AACSDKEventDetail.SubmitButton:
print("Event triggered by a submit button.");
case AACSDKEventDetail.TextLink:
print("Event triggered by a markdown link text.");
case AACSDKEventDetail.UnknownDetail:
print("Event triggered by an unknown component.");
case null:
print("There was no detail provided in this sdk event.");
}
}
});
See the Observing SDK events section for more details on the event observer feature.
Capture image-triggered custom payload
In the Atomic Workbench, the functionality for custom payload has expanded. Initially, you could create a submit or link button with a custom action payload. Now, this capability extends to images, allowing the use of an image with a custom payload to achieve similar interactive outcomes as you would with buttons.
When such an image is tapped, the didTapLinkButton method is called on your action delegate (implementing the AACStreamContainerActionDelegate mixin).
In this scenario, an image is treated similarly to a link button, meaning the same delegate method used for link buttons is applied to images as well.
This approach streamlines the handling of user interactions with both elements, ensuring a concise behavior across the UI.
On iOS, the action's source still tells the two apart: it reports AACCardActionSource.image for an image tap and AACCardActionSource.linkButton for a link button.
The second parameter to this method is an action object, containing the payload that was defined in the Workbench for that button. You can use this payload to determine the action to take, within your app, when the image is tapped.
The action object also contains the card instance ID and stream container ID where the custom action was triggered.
The following code snippet navigates the user to the home screen upon receiving a specific payload.
class MyActionDelegate with AACStreamContainerActionDelegate {
// Provide context to your delegate.
MyActionDelegate(this.context);
final BuildContext context;
// Implement the link button callback from AACStreamContainerActionDelegate
void didTapLinkButton(AACCardCustomAction action) {
// Check the payload.
final screenName = action.actionPayload["screenName"] as String?;
if (screenName != null && screenName == "home-screen") {
// First check if the the widget is still part of the widget tree.
if (context.mounted) {
// Navigate to the home screen
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
);
}
}
}
}
Custom Icons
Note: Requires iOS 16 and above, or Android 7.0 (API 24) and above.
The SDK supports the use of custom icons in card elements. When you are editing a card in the card template editor of the Workbench, you will notice that, for card elements that support it, the properties panel will show an "Include icon" option. From this location you can select an icon to use, either from the Media Library or Font Awesome.
When choosing to use an icon from the Media Library, you have the ability to provide an SVG format icon and an optional fallback icon to be used in case the SVG fails to load. The "Select icon" dropdown will present any SVG format assets in your media library which can be used as a custom icon. To add an icon for use, you can press the "Open Media Library" button at the bottom of the dropdown.
Custom icon colors
The Workbench theme editor now provides the ability to set a color and opacity value for icons in each of the places where an icon may be used. The SDK will apply the following rules when determining what color the provided icon should be displayed in:
- All icons will be displayed with the colors as dictated in the SVG file.
- Black is used if no colors are specified in the SVG file.
- Where a
currentColorvalue is used in the SVG file, the following hierarchy is applied:- Use the icon theme color if this has been supplied.
- Use the color of the text associated with the icon.
- Use a default black color.
Custom icon sizing
The custom icon will be rendered in a square icon container with a width & height in pixels equal to the font size of the associated text. Your supplied SVG icon will be rendered centered inside this icon container, at its true size until it is constrained by the size of container, at which point it will scale down to fit.
Fallback Rules
There are two scenarios where a fallback could occur for an SVG icon:
- If the provided SVG image is inaccessible due to a broken URL or network issues, such as those caused by certificate pinning.
- If the SVG icon is not supported on iOS/Android. Currently, SVG features are not fully supported in the iOS/Android SDK, so please check with our support team for details on supported SVG images.
In these scenarios, the following fallback rules apply:
- The fallback FontAwesome icon is used if it is set in Atomic Workbench for this custom icon.
- Otherwise, a default placeholder image is displayed.
Multiple display heights
You can specify different display heights in Atomic Workbench for banner and inline media components. There are four options, each of which defines how the thumbnail cover of that media is displayed.
- Tall The thumbnail cover is 200 display points high, spanning the whole width and cropped as necessary. This is the default value and matches existing media components.
- Medium The same as "Tall", but only 120 display points high.
- Short The same as "Tall" but 50 display points high. Not supported for inline or banner videos.
- Original The thumbnail cover will maintain its original aspect ratio, adjusting the height dynamically based on the width of the card to avoid any cropping.
Note: For older versions of the SDK, all options will fall back to "Tall".
Retrieving the display height using an API-driven card container
With API-driven card containers, a media element is delivered as an AACMedia component. Its displayHeight property carries the resolved height in display points (the values listed above), and is null for "Original", which sizes the media by its own aspect ratio. See the API-driven card containers section for more information.
The original image dimensions are not provided. To measure them, load the image at AACMedia.thumbnailUrl and check its size.
ProGuard configuration (Android only)
The Atomic SDK needs no ProGuard or R8 rules of its own. If you build with minification
enabled (minifyEnabled true), the default Android rules are enough. You do not need a
proguard-rules.pro file for the SDK, and the keep rules earlier versions of this guide
asked for are no longer required. If your project still contains those rules, remove
them (see Android ProGuard migration).
SDK version compatibility
The Flutter column lists the minimum Flutter version supported by each release. Newer versions are supported too. On iOS, projects with Swift Package Manager disabled fall back to CocoaPods automatically.
The Flutter minimum is unchanged in 26.1.0, but the Android build requirements listed
in the installation steps are not tied to it: your Android Gradle plugin,
Kotlin Gradle plugin and compileSdk are declared by your own project and can be raised
without changing your Flutter version. A Flutter version at the minimum still needs those
requirements met.
| Atomic's Flutter SDK | Flutter | Dart | Android SDK | iOS SDK |
|---|---|---|---|---|
26.1.0 | 3.35.5 | >=3.0.0 <4.0.0 | 26.1.1 | 26.2.0 |
25.2.0 | 3.35.5 | >=3.0.0 <4.0.0 | 25.2.0 | 25.2.0 |
24.2.6 | 3.24.4 | >=3.0.0 <4.0.0 | 24.2.7 | 24.2.11 |
24.2.5 | 3.24.4 | >=3.0.0 <4.0.0 | 24.2.4 | 24.2.10 |
24.2.4 | 3.24.4 | >=3.0.0 <4.0.0 | 24.2.4 | 24.2.5 |
24.2.1 | 3.24.1 | >=3.0.0 <4.0.0 | 24.2.1 | 24.2.0 |
24.2.0 | 3.22.2 | >=3.0.0 <4.0.0 | 24.2.1 | 24.2.0 |
23.4.0 | 3.19.0 | >=2.17.0 <3.0.0 | 23.4.0 | 23.4.0 |
Configuring iPhone mute button functionality
Just apply the same method used in the native iOS SDK docs to the ios/Runner/AppDelegate.swift file in your Flutter app.