Flutter Low-Code Integration

Use the Salesforce Personalization Flutter plugin to display personalized banners, recommendations, and custom components in your Flutter app with minimal setup. The plugin bridges the native iOS and Android Personalization SDKs and provides a declarative ContentZone widget that handles fetching, rendering, and engagement tracking for you. Your app owns native SDK initialization, consent, trusted navigation, and accessible UI.

  1. Install the SDK
  2. Initialize the SDK
  3. Display a Content Zone
  4. Out-of-the-Box Components
  5. Custom Components
  6. Engagement Tracking
  7. Preview
  8. Design-Time Rendering
  9. Identity and Events

Add the package. It resolves to the latest published version.

On iOS, the native pods are pulled in automatically the next time you build (or run pod install). On Android, you must also declare the Salesforce Maven repositories (see Initialize the SDK).

RequirementReviewed baseline
Flutter3.19 or later
Dart3.3 or later
AndroidAPI 26 minimum; compile SDK 37
Java17
Kotlin Gradle plugin2.3.0
Android Gradle Plugin8.13.2
Gradle wrapper8.13
iOS deployment target15.0 or later
Swift5.7 or later

Obtain these values from your Salesforce administrator and inject environment-specific values through the app’s existing build-configuration or secret-management process. The samples show key names only.

KeyValue
salesforce.cdp.appIdCDP application ID
salesforce.cdp.endpointBare endpoint host with no URL scheme
salesforce.cdp.cdnUrlCDN configuration URL
salesforce.cdp.dataspaceDataspace, commonly default

The native SDKs must be initialized before any ContentZone widget attempts to fetch content. Initialization creates the connection to Data 360 (for identity and behavioral data) and the Personalization service (for content decisions). The Flutter plugin does not expose a Dart-side configure(...) call - the host app initializes the native SDK directly in platform code, ensuring it runs before the Flutter engine renders its first frame.

Complete both the Android and iOS platform setup below.

The plugin supplies native SDK dependencies transitively. The host app must provide the Salesforce Maven repositories and a compatible Android toolchain.

Use the reviewed plugins in android/settings.gradle.kts:

Use Gradle 8.13 in android/gradle/wrapper/gradle-wrapper.properties:

Configure android/app/build.gradle.kts. Replace the sample namespace and application ID.

Do not add com.salesforce.personalization:sdk to app dependencies. The plugin exposes it transitively.

Declare INTERNET in the main manifest so release builds can reach the service. Register the custom Application and tenant metadata in android/app/src/main/AndroidManifest.xml. The custom preview scheme below is for controlled development devices only, use verified Android App Links for production preview tokens.

Create MainApplication.kt (or use the existing with the initialization code) under the directory matching its package. This complete example deliberately leaves consent unset.

Set the deployment target to iOS 15 or later and use Swift 5.7 or later. The app target must use use_frameworks!. Flutter autolinking installs the native Salesforce pods; do not declare duplicate Salesforce pods manually.

Add the four salesforce.cdp.* keys inside the existing top-level dict in ios/Runner/Info.plist. The custom preview scheme is for controlled development devices only; use a verified Universal Link for production tokens:

Initialize before registering Flutter plugins. This complete ios/Runner/AppDelegate.swift deliberately leaves consent unset:

After the compatible Flutter package is published and added, confirm the Podfile target uses platform :ios, '15.0' and use_frameworks!, run CocoaPods, and open Runner.xcworkspace.

  • If the native SDK ends up off the classpath (Android) or the pods aren’t linked (iOS), the plugin degrades gracefully - content zones render their fallback and status calls report “not ready” - instead of crashing.
  • To enable debug logging during development: await PersonalizationModule.setLogging(LogLevel.debug);. Call this before the native SFMCSdk.configure(...) / SFMCSdk.initializeSdk(...) call in your platform init code, so logging is active for the whole init sequence. Disable before a production build.

With the SDK initialized and consent granted, place a ContentZone widget in your UI. Pass the content zone identifier and a list of components the zone is allowed to render.

After adding a ContentZone to your app code, create the matching content zone record in Salesforce Personalization. See Set Up Mobile Content Zones for instructions on defining personalization points, assigning components, and configuring engagement definitions.

Personalization must remain disabled until the user explicitly opts in. Await the Dart consent call and persist opt-in only after it succeeds. On opt-out, unmount live zones immediately, call the SDK, then persist the result. Changing consent does not make a mounted zone retry automatically; prefer mounting only while opted in, or refresh an attached controller after a successful change.

Recommendations are eager and not internally scrollable. Put a zone that can render recommendations in a host scrollable. All cards build at once, and built-in View is a mount/render event, not verified on-screen visibility.

The following illustrative widget focuses on update ordering and the required scrollable host. Its loadChoice and saveChoice callbacks are the app’s awaited consent store; null means the user has not made a choice.

ParameterContract
nameRequired content zone identifier. Use a unique identifier per simultaneously mounted zone.
allowedComponentsRequired lookup and security allowlist. Matching is case-insensitive; the first duplicate wins.
loadingOptional initial or explicit loading-state widget.
fallbackOptional bridge, timeout, validation, blocked component, consent, or empty-content fallback. Do not expose raw errors.
timeoutMsNative fetch timeout; default 10000 milliseconds.
controllerOptional programmatic refresh controller.
decisionsRequestContextOptional anchor and attributes merged with automatic context.

The zone rebuilds its component allowlist whenever allowedComponents changes, comparing by value. Changing the allowed component names triggers a refetch; swapping only a component instance for an already-allowed name (for example a new style or onTap) re-renders in place from the retained serving with no refetch, reusing the same ComponentContext so a View is not re-fired. You no longer need to re-key or remount the zone to change the list, though keeping the set of names stable across rebuilds is still good practice.

Use anchorType in DecisionsRequestContext. anchorDmoName is deprecated and retained only as an alias. Attribute values must be String, int, double, or bool; convert dates to strings first.

refresh() keeps old content visible only while the request is pending. A failure replaces it with fallback; refresh(withLoadingState: true) shows loading while pending. Failures do not rethrow; inspect lastRefreshError. dispose() is currently a no-op but should still be called.

Create one ContentZoneController per simultaneously mounted zone. Binding is first-bind-wins and unbinding is ownership-aware, so disposing a second holder no longer unbinds the active zone - but a shared controller still drives only the zone that bound it first, so give each zone its own. Use separate controllers and Future.wait to refresh multiple zones.

App resume, connectivity recovery, identity changes, and decisionsRequestContext changes do not automatically fetch new content. After consent and identity are settled, refresh the dedicated zone controller once when the app’s lifecycle policy requires a new decision.

The plugin ships two ready-to-use components - SalesforceBanner and SalesforceRecommendations - that handle rendering and engagement tracking (View and Click events) automatically.

These components are available out-of-the-box in the UI and are registered under the names "Salesforce_Banner" and "Salesforce_Recommendations".

SalesforceBanner handles Salesforce_Banner. header is required and nonblank. imageUrl is supplied as a string; an invalid or blank value warns and renders an image fallback rather than rejecting an otherwise valid banner. subheader, ctaText, and ctaUrl are optional.

ctaText and ctaUrl are independent. A banner is actionable when an onTap callback exists or ctaUrl is nonblank. onTap wins. Label-only content can render without an action; URL-only content can be actionable without a visible CTA label. Prefer app-owned callbacks to enforce trusted destinations and accessible labels.

The reviewed built-in card uses a bare GestureDetector, provides no explicit button/link semantics or keyboard activation, and truncates text. Use a custom accessible component when those behaviors do not satisfy the app’s accessibility requirements.

SalesforceRecommendations handles Salesforce_Recommendations. Portrait uses one column and landscape uses two. It eagerly builds Column and Row trees; it does not provide a scroll view.

The model has optional sectionHeader, optional shared ctaText, and a nonempty list of valid items. Each item has nonblank id and name, an imageUrl, optional description, and optional item-level url. The plugin does not enforce unique IDs; production payloads must use unique IDs.

Invalid items are skipped, but engagement indices are not remapped. A skipped item can misattribute later View and Click events. Production responses must contain only valid items in unchanged flat serving order. Do not sort, filter, insert, or section before per-item tracking unless each original zero-based index is preserved.

The reviewed renderer defaults are white backgrounds; 20sp semibold #393939 headers; 16sp #6B6B6B subheaders; 15sp underlined #0B5CAB CTAs; 28sp section headers; banner padding 12; recommendation card padding 20; spacing 12; image width fraction 0.3; maximum image size 150; and banner radius 8.

SalesforceBannerStyle.ctaBackgroundColor and imageHeight exist but are not consumed by the reviewed renderer.

Implement a custom component to render a content zone with your own UI. A custom component can reuse an existing backend experience template (for example, clone Salesforce_Banner) or use a fully custom experience template name with its own fields.

Match the component’s name to the backend experience template’s component name. Validate the JSON payload in validateAndCreateComponentModel, and return the widget from compose. See Engagement Tracking for reporting View and Click from a custom component.

Before engagement events are recorded, ensure your engagement definitions (View, Click) are configured in the Data 360 mobile connector. See Set Up Mobile Engagement Tracking for instructions on defining engagement actions alongside your components and content zones.

Out-of-the-box components track View and Click automatically. These are the only two actions currently supported end-to-end. The engagement lifecycle for both built-in and custom components:

  • Built-in Banner invokes its view-once tracker on mount and when personalizationId changes while its widget state remains alive.
  • Built-in Recommendations invokes the per-item view-once tracker for every eagerly rendered index. Item id is not part of the dedup key, and this is not on-screen visibility.
  • A tracker invocation emits only when the expected engagement payload shape contains a matching View payload. A no-op still consumes its once/PID gate.
  • A silent same-ID refresh does not refire while the child state survives. refresh(withLoadingState: true) can refire at the same ID after a loading frame disposes and remounts the child state.
  • A fully disposed and remounted zone can attempt View again at the same ID.
  • Every successful fetch creates a new ComponentContext, but its ID can remain unchanged. Context identity and personalizationId are different signals.
  • On an actionable tap, built-in components invoke Click tracking before the callback or URL action. The action proceeds regardless of payload availability; analytics emit only when a matching payload exists. URL-free cards in the reviewed renderer still install a no-op gesture recognizer.
  • Shape and count mismatches are logged but do not block rendering.
  • Preview commonly has no engagement payloads and is not evidence that production analytics will be emitted.

For custom components:

  • For custom single elements, use trackEngagementViewOnce() for build/mount View and trackEngagement() for repeatable actions.
  • For custom lists, use the per-item variants with original serving indices.
  • View-once methods deduplicate by the exact ComponentContext object. Pass the context supplied by the zone; do not construct or copy one.
  • For true impression measurement, use a custom lazy or paged component and report from an actual visibility signal.

Preview is per zone. A valid receipt causes only its matching zone to refetch; other active zones are unaffected. Query with PersonalizationModule.isPreview(exactZoneName). The plugin’s border is not an accessible status indicator by itself; also show persistent Preview text and Flutter semantics.

The reviewed API exposes only a one-shot preview query, not an observable per-zone preview state for host UI. A persistent synchronized accessible label therefore requires a plugin API or built-in renderer change and remains a publication gate.

Add the separately versioned deep-link dependency validated for the release:

Use app_links for cold and warm links. Register only endpoints your app validates. For HTTPS, configure verified Android App Links and iOS Universal Links for a domain you control. This complete handler awaits the cold link and subscribes for warm links:

Await PreviewLinks.start() before mounting zones, retain the returned owner, and dispose it at app teardown. Treat the full URL and token as sensitive bearer-like material. Never log, persist, analyze, or include them in crashes or support artifacts. Reject untrusted schemes, hosts, paths, ports, user info, fragments, extra query keys, duplicate tokens, and empty tokens.

MockDataContentZone<T> renders components with mock data for styling and layout without any backend setup or networking. It is development/test-only. It accepts one matching Component<T>, MockContent.success(model) or MockContent.failure(error), loading, fallback, mockLoadingMs, and an optional controller. Mock refresh always reruns loading then resolution. Keep component and mock objects stable rather than recreating them on every parent rebuild.

Use a mock controller to exercise refresh and MockContent.failure to verify a customer-safe fallback. Never ship a mock zone in production.

Identity setters and getters are available after native initialization, explicit opt-in, and app authentication:

setAttributes upserts supplied keys and leaves other keys unchanged. Use clearAttribute or clearAllAttributes for attributes. Those methods do not clear profileId or party-identification fields, and this plugin version exposes no public API that clears them. Do not set those identifiers in a multi-user app until the compatible release documents a supported native identity/session reset. Track behavioral events only after the same consent and governance checks.

Mobile backend prerequisite: A mobile personalization point matching ContentZone.name, an assigned component or experience template (built-in names are Salesforce_Banner and Salesforce_Recommendations), payloads that satisfy the contracts above, and View/Click engagement definitions must all be configured and activated in the backend before a zone can return content. See Set Up Mobile Content Zones.