Firebase In-App Messaging in Flutter: Test Devices, Event Triggers, and Custom Rendering
Add firebase_in_app_messaging, prove it works with a test device, then trigger campaigns from Dart and render them with your own widgets.
Published on • October 20, 2026
AI Assistant

Push notifications interrupt people outside your app; Firebase In-App Messaging shows cards, banners, modals, and images to users who are already active in it — a tutorial prompt when they open a feature, a sale banner when they hit the store screen. The Flutter integration is one package plus a small API. Here’s the full loop: install, test on a real device, trigger campaigns from Dart, and optionally render them yourself.
Install the SDK
From the project root, per the Flutter quickstart:
flutter pub add firebase_in_app_messaging
flutter run
import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart';
That’s the whole client setup — the plugin (0.10.0, published October 15, 2026 under the verified firebase.google.com publisher) depends on firebase_core, so make sure Firebase.initializeApp() runs first. The messages themselves are composed in the Firebase console, where you target audiences using Analytics segments or client characteristics.
Prove it works: the installation ID dance
Two constraints make naive testing fail:
- To conserve power, In-App Messaging fetches messages from the server once per day.
- You therefore can’t just “publish and pray.”
The console solves this with test devices keyed by your app’s Firebase installation ID (FID). Grab it from the log output when you run the app. On Android:
I/FIAM.Headless: Starting InAppMessaging runtime with Installation ID YOUR_INSTALLATION_ID
On iOS, add -FIRDebugEnabled to Arguments Passed On Launch in your Xcode scheme, run, and look for:
[Firebase/InAppMessaging][I-IAM180017] Starting InAppMessaging runtime with Firebase Installation ID YOUR_INSTALLATION_ID
Then in the Firebase console (DevOps & Engagement > Messaging), create a campaign with Firebase In-App messages, click Test on Device, paste the installation ID, and hit Test — it sends immediately. You must close and reopen the app on the device to see it. Success looks like Setting this device as a test device (Android) or Seeing test message in fetch response... (iOS) in the logs.
Campaign anatomy and targeting
Worth knowing before you compose anything: message styles are cards, banners, modals, or images, chosen in the console, and the Firebase console lets you “set up triggers so that they appear exactly when they’d benefit your users most.” Targeting runs on Firebase Analytics events and attributes plus imported segments — client characteristics, demographics, or past behavior — so the audience definition and the trigger event should be designed together (an in-store message needs both an audience that shops and a store_opened trigger). For iteration, campaigns can also be run as In-App Messaging experiments with A/B Testing to compare variants rather than guessing.
Campaign triggers: Firebase-prompted vs. event-driven
By default, the SDK decides when to display a fetched message — that’s the Firebase-prompted path: the native SDK shows the campaign when it decides the moment is right (for example, on app foreground). For anything contextual, you drive it from code with a named event that you configured as a trigger when composing the campaign:
await FirebaseInAppMessaging.instance.triggerEvent('store_opened');
triggerEvent(eventName) is the only trigger primitive — match the string to the event trigger you set up in the console, and the campaign tied to it appears (once its fetch conditions are satisfied). Two switches matter when you need to hold messages back:
// Suppress all in-app messages (e.g., during onboarding or a full-screen flow)
await FirebaseInAppMessaging.instance.setMessagesSuppressed(true);
// Opt out of automatic analytics data collection for this plugin
await FirebaseInAppMessaging.instance.setAutomaticDataCollectionEnabled(false);
Watch the message lifecycle
Four streams report what happened to each campaign, which is where analytics and navigation logic live:
final fiam = FirebaseInAppMessaging.instance;
fiam.onMessageImpression.listen((e) => print('shown: ${e.campaignMetadata.campaignName}'));
fiam.onMessageClicked.listen((e) {
print('${e.campaignMetadata.campaignName} -> ${e.action.actionUrl}');
// handle navigation yourself — see the warning below
});
fiam.onMessageDismissed.listen((e) => print('dismissed: ${e.dismissType}'));
fiam.onMessageDisplayError.listen((e) => print('failed to render: ${e.error}'));
Note the plugin does not open action URLs for you — when a user taps a button, it’s on your code to route (or report the interaction, as described next).
Custom Flutter display: your own widgets instead of native templates
If the native cards don’t match your design system, opt into custom rendering. The native SDK still fetches and decides, but forwards the campaign to Flutter instead of drawing it:
fiam.onMessageDisplay.listen((message) async {
await message.impress(); // keep campaign analytics correct
// Render your own widget with message.texts / message.image / message.action
// Then report what the user did:
// await message.click(message.action!);
// await message.dismiss();
});
await FirebaseInAppMessaging.instance.setCustomDisplayEnabled(true);
The contract is strict: the plugin won’t report anything on your behalf in this mode, so you must call impress(), click(), and dismiss() yourself or campaign metrics silently rot. The supporting types (InAppMessage, InAppMessageText, InAppMessageAction, InAppMessagingCampaignMetadata, and the InAppMessageType enum for the console-chosen layout) all ship in the same package.
A practical rollout order
- Install, initialize
firebase_core, run once to capture the FID. - Send a test message from the console and confirm the reopen-and-see loop.
- Compose a real campaign with an Analytics-based audience; leave display Firebase-prompted until you trust the cadence.
- Add
triggerEventcalls at genuinely contextual moments — resist firing them everywhere. - Add suppression around full-screen flows so messages never compete with your own UI.
- Only then consider
setCustomDisplayEnabled, with impress/click/dismiss handling wired before it goes live.