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. |