Configure App Frames

Prev Next

This page explains how to display App Frames content in your Flutter app with the InsiderAppFramesView widget. The widget embeds the native iOS and Android App Frames views, so the content, analytics, and refresh behavior are the same as in a native app.

This widget is available on Flutter SDK 5.3.0 or higher and requires Flutter 3.0.0 or higher.

Overview

App Frames renders marketer-managed content inline, as a natural part of your screen: a banner on the home page, an offer inside a product list, an announcement above the checkout button. Unlike push notifications or in-app messages, a frame is not an overlay. It is a component you place in your own layout, and you manage its content from the Insider One panel without a new app release.

  • Automatic refresh: Frames refresh when the app returns to the foreground and when the user identity changes.

  • In-memory caching: Already loaded content re-renders instantly, without a network request.

  • Automatic analytics: The SDK tracks impressions, clicks, and dismissals.

  • Marketer-side control: The panel manages targeting and up to 8 campaign variants per placement.

During a successful load, a frame moves through these statuses. ready is the only status in which content is visible; every other status keeps the frame collapsed.

resolving → downloading → rendering → ready

Before you start

  • Opt-in: App Frames is off by default. Until you enable it (Step 1), every frame stays empty.

  • Fail-silent: An empty placement renders nothing. Do not reserve fixed space for a frame.

  • Initialize first: Mount the widget after FlutterInsider.Instance.init(...). A frame created before initialization does not load.

  • Supported platforms: iOS and Android. On web and desktop, the widget renders nothing.

1. Enable App Frames

Add the opt-in flag to both native projects of your Flutter app.

iOS: In ios/Runner/Info.plist, add the App Frames Enabled key inside the Insider dictionary. If your Info.plist already has an Insider dictionary, add the key to it.

Android: In android/app/src/main/AndroidManifest.xml, add the meta-data inside the <application> tag.

Info.plist

<key>Insider</key>
<dict>
    <key>App Frames Enabled</key>
    <true/>
</dict>

AndroidManifest.xml

<application ...>
    <meta-data
        android:name="com.useinsider.insider.APP_FRAMES_ENABLED"
        android:value="true" />
</application>

If the flag is missing or misspelled, frames report the disabled status and render nothing. No other error is reported.

2. Create a placement

Define your placement in the panel under Mobile App Settings › App Frames Placement. Note the placement ID; you pass the same value to the widget.

3. Add the widget

Place InsiderAppFramesView where the content should appear, for example inside a Column or ListView. The widget fills the available width and manages its own height.

The following table shows the data type for the widget's parameters.

Parameter

Data Type

Description

placementId

String (required)

The placement ID defined in the panel.

height

double?

Fixed height in logical pixels. Leave it null (default) so the widget sizes itself to the content.

onStatusChanged

Function(status, previousStatus)?

Called on every status change. See Track the frame status.

onLoadFailed

ValueChanged<InsiderAppFramesError>?

Called when the content fails to load. Diagnostic only.

onHeightChanged

ValueChanged<double>?

Called when the content reports a new height, in logical pixels.

onDismissRequested

VoidCallback?

Called when the user closes the content.

onActionTriggered

ValueChanged<Map<String, dynamic>>?

Called with the custom data of an action defined in the campaign.

collapseOnDismiss

bool

Whether the widget collapses when the user closes the content. Default: true.

gestureRecognizers

Set<Factory<OneSequenceGestureRecognizer>>?

Gestures the frame can claim from surrounding widgets. Default: taps only.

Method Signature

const InsiderAppFramesView({
  Key? key,
  required String placementId,
  double? height,
  void Function(
    InsiderAppFramesViewStatus status,
    InsiderAppFramesViewStatus previousStatus,
  )? onStatusChanged,
  ValueChanged<InsiderAppFramesError>? onLoadFailed,
  ValueChanged<double>? onHeightChanged,
  VoidCallback? onDismissRequested,
  ValueChanged<Map<String, dynamic>>? onActionTriggered,
  bool collapseOnDismiss = true,
  Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
})

Method Example

import 'package:flutter/material.dart';
import 'package:flutter_insider/flutter_insider.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        const Text('Welcome'),
        // Renders nothing when there is no active campaign for "home_page".
        const InsiderAppFramesView(placementId: 'home_page'),
        const Text('Recommended for you'),
      ],
    );
  }
}

4. Track the frame status

Use onStatusChanged to observe the frame's lifecycle. It is called with the new status and the previous one. The isLoading and isError extension getters let you group statuses without matching each case.

InsiderAppFramesView(
  placementId: 'home_page',
  onStatusChanged: (status, previousStatus) {
    if (status == InsiderAppFramesViewStatus.ready) {
      // Content is visible.
    } else if (status.isLoading) {
      // resolving, downloading, or rendering.
    } else if (status.isError) {
      // errorResolving, errorDownloading, or errorRendering.
    }
  },
)

Status

Description

detached

The frame is not on screen. Initial status. Content that was already loaded is kept.

noPlacement

The frame is on screen but has no placement ID.

disabled

App Frames is not enabled for the app. Check Step 1. Not an error.

resolving

Finding the campaign that targets the placement.

downloading

Downloading the campaign content.

rendering

Rendering the content. The frame is not visible yet.

ready

The content is rendered. The only visible status.

unavailable

No active campaign targets the placement. Not an error.

dismissed

The user closed the content.

errorResolving

The campaign could not be resolved.

errorDownloading

The content could not be downloaded, or its URL failed validation.

errorRendering

The content was downloaded but could not be displayed.

unknown

A status from a newer native SDK that this plugin version does not recognize. The frame collapses.

New statuses may be added in future versions. When you switch over InsiderAppFramesViewStatus, always include a default clause.

5. Handle dismissal

When the user closes the content (for example, with the close button of the template), the widget collapses to zero height and onDismissRequested is called. The frame stays closed for the lifetime of that widget instance.

  • To show the frame again, give the widget a new Key or a different placementId.

  • To control dismissal yourself, set collapseOnDismiss: false and remove or hide the widget in onDismissRequested.

bool _showBanner = true;

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      if (_showBanner)
        InsiderAppFramesView(
          placementId: 'home_page',
          collapseOnDismiss: false,
          onDismissRequested: () => setState(() => _showBanner = false),
        ),
    ],
  );
}

6. Handle actions

A campaign can attach custom data to a button. When the user taps it, onActionTriggered is called with that data as a map. The callback is called only when the action carries data.

InsiderAppFramesView(
  placementId: 'home_page',
  onActionTriggered: (Map<String, dynamic> data) {
    final coupon = data['coupon'];
    if (coupon is String) {
      // Apply the coupon in your app.
    }
  },
)

The SDK has already opened any deep link on the action before this callback is called. The callback is a notification; you cannot cancel the navigation from it.

7. Height

By default the widget sizes itself to the content. It starts at a 1 logical-pixel placeholder, grows to the content height when the content is ready, and shrinks back when the content goes away (for example, unavailable or an error).

  • To use a fixed height, pass height. The fixed height is always used, regardless of the content height.

  • onHeightChanged is called with every height the content reports, including when you pass a fixed height.

Do not wrap the widget in a parent with a fixed height or zero height. Let the widget manage its own size.

8. Gestures inside scroll views

By default, the frame claims taps only, so buttons and the close control work while a surrounding ListView keeps scrolling. If a template needs other gestures, such as a horizontal carousel, pass them with gestureRecognizers.

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';

InsiderAppFramesView(
  placementId: 'home_carousel',
  gestureRecognizers: {
    Factory<OneSequenceGestureRecognizer>(() => TapGestureRecognizer()),
    Factory<OneSequenceGestureRecognizer>(() => HorizontalDragGestureRecognizer()),
  },
)

9. Handle errors

onLoadFailed is called with an InsiderAppFramesError when a frame fails to load. Use it for logging. Do not show an error to the end user; the frame already collapses.

Property

Data Type

Description

code

InsiderAppFramesErrorCode

The error code. See the table below.

message

String

A description of the error.

dismissCode

int?

The dismiss code reported by the template, if any. Android only; always null on iOS.

cause

String?

The underlying native error, if any. For diagnostics only.

Error code

Status

Description

resolutionFailed

errorResolving

The request that resolves placements failed (network or server error).

responseMalformed

errorResolving

The placements response could not be read.

downloadingFailed

errorDownloading

The content could not be downloaded.

placementUntrusted

errorDownloading

The content URL is not a valid http(s) URL, so it was not loaded.

contentDisplayFailed

errorRendering

The content was downloaded but could not be displayed.

renderingFailed

errorRendering

The template reported an error while running.

unknown

-

An unclassified error, or a code this plugin version does not recognize.

InsiderAppFramesView(
  placementId: 'home_page',
  onLoadFailed: (InsiderAppFramesError error) {
    debugPrint('App Frame failed: ${error.code} ${error.message}');
  },
)

Full example

The following screen shows two placements in a scrollable list and uses every callback.

import 'package:flutter/material.dart';
import 'package:flutter_insider/flutter_insider.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: ListView(
        children: [
          InsiderAppFramesView(
            placementId: 'home_top_banner',
            onStatusChanged: (status, previousStatus) {
              debugPrint('home_top_banner: $previousStatus -> $status');
            },
            onLoadFailed: (error) {
              debugPrint('home_top_banner failed: ${error.code}');
            },
            onHeightChanged: (height) {
              debugPrint('home_top_banner height: $height');
            },
            onDismissRequested: () {
              debugPrint('home_top_banner dismissed');
            },
            onActionTriggered: (data) {
              debugPrint('home_top_banner action: $data');
            },
          ),
          const ListTile(title: Text('Categories')),
          const ListTile(title: Text('Best sellers')),
          const InsiderAppFramesView(placementId: 'home_mid_offer'),
          const ListTile(title: Text('Recommended for you')),
        ],
      ),
    );
  }
}

Limitations

  • A placement can have at most 10 active campaigns.

  • Each image asset can be at most 500 KB.

  • Content is cached in memory only; it is not persisted across app launches.

  • A new placement requires a developer to add a widget with its placement ID to the app.

  • Frames load only when GDPR consent and mobile app access are granted. When consent is revoked, frames move to disabled.

Troubleshooting

Symptom

Cause

Fix

Nothing is shown; status is disabled

The opt-in flag is missing, or consent is not granted.

Check Step 1 on both platforms (the key must be inside the Insider dictionary on iOS). Check GDPR consent and mobile app access.

Nothing is shown; status is unavailable

No active campaign targets the placement.

Check that the placement ID matches the panel exactly and that the campaign is active and targets this user.

The frame never loads

The widget was mounted before the SDK was initialized.

Mount the widget after FlutterInsider.Instance.init(...) completes.

Buttons in the content do not respond

A custom gestureRecognizers set without a tap recognizer.

Include TapGestureRecognizer, or omit the parameter to use the default.

Content is cut off or has extra space

A fixed height, or a parent with a fixed size.

Remove the fixed height so the widget sizes itself.

A closed frame does not come back

Dismissal is kept for the widget instance.

Give the widget a new Key, or see Handle dismissal.