EmbedWidget — Advanced FAB Visibility (Flutter)
Advanced control over when and where the floating agent button (FAB) appears
in your Flutter app: route allow-lists, multi-flow grouping, pre-show delays +
policies, per-flow positioning, and continuity.
This is the Flutter counterpart of the React Native
EmbedProvider Advanced guide.
Terminology: A React “group” maps to a Flutter “flow” — the string
key used in enabledRoutes, groupDelays, groupContinuity, and
groupInsets. Throughout this doc, flow = group.
Overview
The FAB becomes visible on a screen only when all of these are true:
| Gate | Controlled by |
|---|
| Widget enabled | showEmbedWidget: true |
| Current route is in an enabled flow (or all routes) | enabledRoutes / showOnAllRoutes |
| Route is not blacklisted | disabledRoutes |
| Agent is live for the flow | backend config (is_live) |
| Call channel initialized | internal (LiveKit) |
| Pre-show delay elapsed | embedButtonDelayMs / groupDelays |
Two integration surfaces:
EmbedWidget — full-control widget that wraps your app. Exposes every
visibility option below.
EmbedProvider — convenience wrapper that also calls embedInitialize
for you. Exposes the common subset (see
Differences).
Prerequisites
| Requirement | Notes |
|---|
embed_flutter installed | added to pubspec.yaml |
| SDK initialized | embedInitialize(apiKey, flowName: ...) (or use EmbedProvider) |
| Route detection wired | EmbedNavigatorObserver, EmbedRouteListener, or manual setCurrentScreen |
| Route names match | the names you pass to enabledRoutes must match the names your router reports |
Flutter integration (install, native setup, basic EmbedWidget)
Start here if you have not finished installation, Android/iOS native setup,
embedInitialize, and a minimal EmbedWidget around your MaterialApp.
Basic setup
Step 1 — Minimal (show on all routes)
EmbedWidget(
showOnAllRoutes: true,
child: MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()],
home: const HomeScreen(),
),
);
Step 2 — Allow-list specific routes (a flow)
EmbedWidget(
enabledRoutes: const {
'main': ['home_screen', 'profile_plans'],
},
child: MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()],
home: const HomeScreen(),
),
);
Step 3 — Add a global pre-show delay
EmbedWidget(
enabledRoutes: const {'main': ['home_screen', 'profile_plans']},
embedButtonDelayMs: 3000, // FAB appears 3s after entering an enabled route
child: /* ... */,
);
Step 4 — Full per-flow groups (delay policy, continuity, position)
EmbedWidget(
enabledRoutes: const {
'main': ['home_screen', 'profile_plans'],
'checkout': ['cart_screen', 'payment_screen'],
},
embedButtonDelayMs: 2000, // fallback for flows without a groupDelays entry
groupDelays: const {
'main': EmbedButtonDelay(
delayMs: 4000,
policy: EmbedButtonDelayPolicy.perScreen,
),
'checkout': EmbedButtonDelay(
delayMs: 1000,
policy: EmbedButtonDelayPolicy.oncePerAppSession,
),
},
groupContinuity: const {
'checkout': EmbedButtonContinuity.continuous, // stay visible within flow
},
groupInsets: const {
'checkout': EmbedButtonInset(right: 16, bottom: 120),
},
child: /* ... */,
);
How screen-based visibility works
The SDK keeps a single source of truth in EmbedRouteManager (a singleton):
the current route and the current flow. On every route change the manager
re-evaluates whether the FAB should be active for that route.
route change ──▶ EmbedRouteManager.updateRoute(name)
│ resolves flow via enabledRoutes + RouteMatchMode
▼
isEmbedActive? ──▶ EmbedWidget shows / hides the FAB
Route names are normalized so a leading slash is ignored — /home and
home match the same pattern. This makes GoRouter (which reports /home) and
the standard Navigator (which may use home) interoperable.
Route detection options
Pick whichever fits your routing setup. All three feed the same
EmbedRouteManager.
1. EmbedNavigatorObserver (recommended for Navigator / GoRouter)
MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()],
// ...
);
For GoRouter (which reports full paths) or any custom naming, supply
nameExtractor to map a route to the name used in enabledRoutes:
EmbedNavigatorObserver(
nameExtractor: (route) {
final path = route.settings.name ?? '';
if (path.startsWith('/home')) return 'home_screen';
// The app's home/initial route is reported as '/' — map it explicitly:
if (path == '/' ) return 'welcome_screen';
return path.isNotEmpty ? path : null; // return null to ignore a route
},
);
2. EmbedRouteListener (declarative, per-screen)
Wrap a screen’s body; it reports routeName to the manager on build:
EmbedRouteListener(
routeName: 'home_screen',
child: HomeScreenBody(),
);
3. EmbedRouteManager().setCurrentScreen(...) (manual)
Framework-agnostic — call from a screen’s initState (useful for shell routes,
custom page managers, or the app’s initial home: route whose name is /):
@override
void initState() {
super.initState();
EmbedRouteManager().setCurrentScreen('welcome_screen');
}
Global delay
embedButtonDelayMs keeps the FAB hidden for N milliseconds after the user
lands on an enabled route, then shows it with animation. The timer resets when
navigating away and back (subject to delay policy).
It is the fallback delay: any flow without its own groupDelays entry uses
this value. Default 0 (appear immediately).
The nudge/inactivity countdown starts only after the FAB becomes visible
(i.e. after this delay), not on route entry.
Visibility groups (flows)
A flow groups a set of routes under shared behavior. Define routes with
enabledRoutes; attach per-flow behavior with the parallel maps groupDelays,
groupContinuity, and groupInsets (all keyed by the same flow name).
EmbedButtonDelay({
required int delayMs,
EmbedButtonDelayPolicy policy = EmbedButtonDelayPolicy.perScreen,
});
| Value | Behavior |
|---|
perScreen (default) | Delay runs on every screen visit within the flow. With continuity: keepVisible, an already-visible FAB stays up (no re-delay). |
oncePerGroupEntry | Delay runs only when entering the flow; screens visited while staying in the flow skip it. Leaving and re-entering re-applies it. |
oncePerAppSession | Delay runs only the first time the flow is shown in the app session; later visits show immediately. |
Parse from a string with EmbedButtonDelayPolicy.fromString('oncePerGroupEntry').
| Value | Alias | Behavior |
|---|
keepVisible | continuous | FAB stays visible when navigating between screens in the same flow — no hide/show cycle, no re-delay. |
reset | perScreen | FAB hides then re-shows on every route change, even within the flow. |
Aliases match the React API; EmbedButtonContinuity.continuous == keepVisible
and EmbedButtonContinuity.perScreen == reset. Parse with
EmbedButtonContinuity.fromString('continuous').
Delay-resolution chain
groupDelays[flow].delayMs → embedButtonDelayMs → 0
Insets (positioning)
Default position is bottom-right, derived from rightPadding / bottomPadding
(logical pixels). Override per flow with groupInsets:
class EmbedButtonInset {
final double right; // default 20.0
final double bottom; // default 100.0
const EmbedButtonInset({this.right = 20.0, this.bottom = 100.0});
}
groupInsets: const {
'checkout': EmbedButtonInset(right: 16, bottom: 120),
},
A matching flow’s inset takes precedence over rightPadding / bottomPadding.
The backend UI config may also carry a widget_position
(bottom-left / top-right / …) and padding that influence the corner the
FAB anchors to.
Configuration reference
| Prop | Type | Default | Description |
|---|
child | Widget | — | Your app (typically MaterialApp). |
showEmbedWidget | bool | true | Master on/off for the FAB. |
apiKey | String? | — | Optional; usually set via embedInitialize. |
embedUrl | String? | — | Backend URL override. |
enabledRoutes | Map<String, List<String>>? | — | Flow → routes the FAB is allowed on. |
showOnAllRoutes | bool | false | Show on every route (overrides enabledRoutes). |
disabledRoutes | List<String>? | — | Routes where the FAB is always hidden (blacklist). |
routeMatchMode | RouteMatchMode | exact | How routes match patterns: exact / contains / startsWith. |
embedButtonDelayMs | int | 0 | Global pre-show delay; fallback for flows without groupDelays. |
groupDelays | Map<String, EmbedButtonDelay>? | — | Per-flow delay + EmbedButtonDelayPolicy. |
groupContinuity | Map<String, EmbedButtonContinuity>? | — | Per-flow continuity (keepVisible/reset). |
groupInsets | Map<String, EmbedButtonInset>? | — | Per-flow position override. |
rightPadding | double? | 20 | Default FAB right padding (dp). |
bottomPadding | double? | 100 | Default FAB bottom padding (dp). |
onPermissionStatusChanged | void Function(bool)? | — | Microphone permission callback. |
EmbedProvider props
Convenience wrapper; also calls embedInitialize. Exposes a subset:
| Prop | Type | Default |
|---|
child | Widget | — |
apiKey | String | — (required) |
embedUrl | String? | — |
flowName | String? | — |
enabledRoutes | Map<String, List<String>>? | — |
showOnAllRoutes | bool | false |
disabledRoutes | List<String>? | — |
routeMatchMode | RouteMatchMode | exact |
embedButtonDelayMs | int | 0 |
groupDelays | Map<String, EmbedButtonDelay>? | — |
rightPadding / bottomPadding | double? | 20 / 100 |
onPermissionStatusChanged | void Function(bool)? | — |
onInitResult | void Function(bool, String?)? | — |
EmbedProvider does not currently expose groupInsets or
groupContinuity. Use EmbedWidget directly if you need those.
Types
| Type | Members |
|---|
RouteMatchMode | exact, contains, startsWith |
EmbedButtonDelay | delayMs: int, policy: EmbedButtonDelayPolicy |
EmbedButtonDelayPolicy | perScreen, oncePerGroupEntry, oncePerAppSession + fromString |
EmbedButtonContinuity | keepVisible(=continuous), reset(=perScreen) + fromString |
EmbedButtonInset | right: double = 20, bottom: double = 100 |
Usage examples
Example 1 — Multi-screen flow + one-shot confirmation flow
EmbedWidget(
enabledRoutes: const {
'shopping': ['catalog', 'product', 'cart'],
'confirm': ['order_confirmed'],
},
groupDelays: const {
'shopping': EmbedButtonDelay(
delayMs: 3000,
policy: EmbedButtonDelayPolicy.oncePerGroupEntry,
),
'confirm': EmbedButtonDelay(
delayMs: 0, // immediate on the confirmation screen
policy: EmbedButtonDelayPolicy.oncePerAppSession,
),
},
groupContinuity: const {
'shopping': EmbedButtonContinuity.continuous,
},
child: /* ... */,
);
Example 2 — Two single-screen flows with different delays
EmbedWidget(
enabledRoutes: const {
'help': ['support'],
'billing': ['invoices'],
},
groupDelays: const {
'help': EmbedButtonDelay(delayMs: 1000),
'billing': EmbedButtonDelay(delayMs: 5000),
},
child: /* ... */,
);
Example 3 — Global delay only (no per-flow config)
EmbedWidget(
enabledRoutes: const {'main': ['home', 'profile']},
embedButtonDelayMs: 2000, // applies to every enabled route
child: /* ... */,
);
Practical scenarios
| Scenario | Configuration |
|---|
| Let users read a form before the FAB appears | embedButtonDelayMs: 4000 (or a per-flow EmbedButtonDelay). |
| Keep the FAB steady across a multi-step flow | groupContinuity: {'flow': EmbedButtonContinuity.continuous} + oncePerGroupEntry delay. |
| Show the FAB only once per session, then instantly | EmbedButtonDelay(delayMs: 2000, policy: oncePerAppSession). |
| Avoid overlapping a bottom bar on one flow | groupInsets: {'flow': EmbedButtonInset(bottom: 140)}. |
| Hide the FAB on auth/splash screens | omit them from enabledRoutes, or add to disabledRoutes. |
Differences from the React provider
| React Native | Flutter | Notes |
|---|
includeScreens | enabledRoutes | Flutter groups routes under a flow key. |
embedButtonVisibilityConfig (single object) | groupDelays + groupContinuity + groupInsets (parallel maps) | Same capabilities, different shape. |
group delayMs / delayPolicy | EmbedButtonDelay | ✅ |
group continuity (continuous/perScreen) | EmbedButtonContinuity (+ aliases) | ✅ |
embedButtonDelayMs | embedButtonDelayMs | ✅ |
defaultDelayMs | — | covered by global embedButtonDelayMs. |
EmbedButtonInset { top, right, bottom, left } | EmbedButtonInset { right, bottom } | ⚠️ Flutter supports right/bottom only. |
defaultInset | — | use rightPadding/bottomPadding. |
navigationRef | EmbedNavigatorObserver / EmbedRouteListener / setCurrentScreen | route detection. |
appVersion | — | not exposed. |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|
| FAB never appears on a route | route name doesn’t match enabledRoutes | confirm the name your router reports; use nameExtractor or setCurrentScreen. |
| FAB missing on the initial/home screen | home: route is reported as /, not your name | map / in nameExtractor, or call setCurrentScreen('...') in that screen’s initState. |
| FAB appears off-screen | stale/zero screen size at first layout | ensure you’re on a recent SDK (position is recomputed post-layout). |
| Delay never elapses / FAB stays hidden | very large delayMs, or route flips inactive before it fires | verify the route stays in the flow; check embedButtonDelayMs/groupDelays. |
| FAB flickers between screens in a flow | continuity is reset | set EmbedButtonContinuity.keepVisible (continuous) for that flow. |
Best practices
- Match route names exactly to what your router reports (after slash
normalization). Prefer one detection mechanism (observer or listener or
manual) per app.
- Use flows as groups — one flow per logical journey (checkout, onboarding).
- Prefer
oncePerGroupEntry / oncePerAppSession for delays inside
multi-screen flows so users aren’t re-delayed on every screen.
- Pair
continuous continuity with oncePerGroupEntry for a steady FAB
across a flow.
- Keep delays short (1–4s) — long delays read as “no button”.
- Use
groupInsets to dodge bottom bars/FABs on specific flows rather than
moving the global padding.
Back to Flutter integration
Installation, native LiveKit setup, embedInitialize, navigator
integration, events, and troubleshooting.
React Native — EmbedProvider advanced
The React Native counterpart: includeScreens, embedButtonDelayMs, and
embedButtonVisibilityConfig.
For additional help visit https://docs.revrag.ai/embed/integration/flutter or email contact@revrag.ai.