embed_flutter 0.1.0
embed_flutter: ^0.1.0 copied to clipboard
A powerful Flutter SDK that provides a powerful voice-enabled AI-agent functionality with real-time widget tree monitoring for screen context fetching and realtime voice communication.
Embed Flutter SDK #
A powerful Flutter SDK that provides a powerful voice-enabled AI-agent functionality with real-time widget tree monitoring for screen context fetching and realtime voice communication.
Table of Contents #
Installation #
Add the following dependency to your pubspec.yaml:
dependencies:
embed_flutter: ^0.0.15
Then run:
flutter pub get
Android Configuration
Add the following permissions to your android/app/src/main/AndroidManifest.xml:
<!-- Required permissions for Embed SDK -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MICROPHONE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Add the following permissions for embed -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MICROPHONE" />
iOS Configuration
1. Add microphone permission to Info.plist
Add the following permissions to your ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for voice calls.</string>
2. Configure Podfile for microphone permission
The SDK requires explicit configuration in your ios/Podfile to enable microphone permission requests. Add the following configuration to your post_install block:
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=1',
# ...other permissions...
]
end
end
end
Important: Without the
PERMISSION_MICROPHONE=1macro in the Podfile, the microphone permission dialog will not appear on iOS and the permission will be reported as permanently denied even if it hasn't been requested before.
Getting Started #
Initialization #
Before using any EmbedWidget, you must initialize the configuration once in your main.dart. You can initialize multiple API keys for different flows:
void main() {
// Initialize API keys for different flows
embedInitialize(
'api-key-for-flow-1',
flowName: 'flow 1', // Optional: Flow identifier
embedUrl: 'https://embed.revrag.ai', // Optional
);
embedInitialize(
'api-key-for-flow-2',
flowName: 'flow 2', // Optional: Flow identifier
embedUrl: 'https://embed.revrag.ai', // Optional
);
runApp(const MyApp());
}
Note: Each
embedInitialize()call saves the API key for that specific flow. When a user navigates to a route, the SDK automatically detects which flow the route belongs to and uses the corresponding API key.
Required: Navigator Observer
For route-based activation to work properly, you must add EmbedNavigatorObserver to your app's navigator observers. This enables the SDK to track route changes and activate/deactivate the EmbedWidget accordingly.
For GoRouter:
import 'package:go_router/go_router.dart';
import 'package:embed_flutter/embed_flutter.dart';
final GoRouter router = GoRouter(
observers: [EmbedNavigatorObserver()], // Required for route tracking
routes: [
// Your routes here
],
);
For MaterialApp:
import 'package:embed_flutter/embed_flutter.dart';
MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()], // Required for route tracking
// Other MaterialApp properties
)
Required: USER_DATA Event
To activate the EmbedWidget on the screen, you must call the USER_DATA embedEvent with a user ID. This is typically done after user authentication or when you have a user identifier:
import 'package:embed_flutter/embed_flutter.dart';
// Call this after user authentication or when you have a user ID
embedEvent(
EventKeys.USER_DATA,
UserEventPayload(
app_user_id: 'user_12345', // Required: Unique user identifier
data: {
'name': 'John Doe', // Optional: Additional user data
'email': 'john@example.com',
'phone': '+1234567890',
},
),
);
Important: The EmbedWidget will not be fully functional until the
USER_DATAevent is sent. This event initializes the voice communication features.
Optional: SCREEN_STATE Event
You can optionally send SCREEN_STATE events to provide additional context about screen changes or user navigation:
// Send screen state when navigating to a new screen
embedEvent(
EventKeys.SCREEN_STATE,
ScreenEventPayload(
screen: 'product_selection_screen', // Required: Screen identifier
data: {
'category': 'banking', // Optional: Additional screen context
'user_type': 'premium',
'flow_step': 'selection',
},
),
);
Optional: CUSTOM_EVENT
CUSTOM_EVENT lets you capture bespoke analytics or interaction events that matter to your flows (for example, plan selections, button taps, or offer views). Supply any key/value metadata you want to track:
embedEvent(
EventKeys.CUSTOM_EVENT,
CustomEventPayload(
data: {
'context': 'pricing_screen',
'event': 'option_selected',
'option_id': 'plan_12_months',
'price_per_month': 249,
'currency': 'INR',
},
),
);
Tip: Combine
CUSTOM_EVENTwith your flow configuration to build rich analytics around user journeys.
Optional: ANALYTICS_DATA Event
ANALYTICS_DATA is similar to CUSTOM_EVENT but requires an explicit event name. Use this when you need to track structured analytics events with a specific event identifier. This is useful for tracking user actions, conversions, permission status changes or other analytics events that need to be categorized by name:
embedEvent(
EventKeys.ANALYTICS_DATA,
AnalyticsDataEventPayload(
event_name: 'purchase_completed', // Required: Event identifier
data: {
'product_id': 'prod_12345',
'amount': 99.99,
'currency': 'USD',
'payment_method': 'credit_card',
},
),
);
Tip: Use
ANALYTICS_DATAwhen you need to track events with a specific name for analytics purposes, whileCUSTOM_EVENTis better for general-purpose custom tracking without a named event.
Available Events
| Event | Purpose | Required | When to Use |
|---|---|---|---|
USER_DATA |
Initialize user context and activate EmbedWidget | Required | After user authentication or when user ID is available |
SCREEN_STATE |
Provide screen context and navigation info | Optional | When navigating between screens or when screen context changes |
CUSTOM_EVENT |
Capture bespoke user interactions or analytics | Optional | Whenever you need additional tracking for specific actions |
ANALYTICS_DATA |
Track structured analytics events with event name | Optional | When tracking named analytics events (e.g., conversions, purchases, permission status changes) |
Basic Usage #
Here's a complete example showing how to use the SDK with flow-based activation:
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';
void main() {
// 1. Initialize the SDK with flow-based API keys
embedInitialize('api-key-for-flow-1', flowName: 'flow 1');
embedInitialize('api-key-for-flow-2', flowName: 'flow 2');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return EmbedWidget(
showEmbedWidget: true,
// 4. Configure flow-based activation
enabledRoutes: {
'flow 1': ['home', 'product_selection_screen'],
'flow 2': ['checkout', 'payment'],
},
child: MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()], // Required for route tracking
routes: [
'home': (context) => const MyHomePage(),
'product_selection_screen': (context) => const ProductSelectionScreen(),
'checkout': (context) => const CheckoutScreen(),
'payment': (context) => const PaymentScreen(),
]
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
// 2. Send USER_DATA event to activate EmbedWidget
// This should typically be called after user authentication
embedEvent(
EventKeys.USER_DATA,
UserEventPayload(
app_user_id: 'user_12345', // Replace with actual user ID
data: {
'name': 'John Doe',
'email': 'john@example.com',
'plan': 'premium',
},
),
);
}
void _navigateToProductScreen() {
// 3. Send SCREEN_STATE event for additional context (Optional)
embedEvent(
EventKeys.SCREEN_STATE,
ScreenEventPayload(
screen: 'product_selection_screen',
data: {
'category': 'banking_products',
'user_segment': 'new_customer',
},
),
);
// Navigate to product screen
Navigator.pushNamed(context, 'product_selection');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My App')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Welcome to My App!'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _navigateToProductScreen,
child: const Text('Go to Product Selection'),
),
],
),
),
);
}
}
Required Parameters #
child(required): The main content of your app
Flow-Based Activation #
The SDK supports intelligent flow-based activation, allowing you to organize routes into flows and assign different API keys to each flow. When a user navigates to a route, the SDK automatically detects which flow the route belongs to and uses the corresponding API key.
Basic Flow Configuration
void main() {
// Initialize API keys for different flows
embedInitialize('api-key-for-flow-1', flowName: 'flow 1');
embedInitialize('api-key-for-flow-2', flowName: 'flow 2');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return EmbedWidget(
// Configure flows with their associated routes
enabledRoutes: {
'flow 1': [
'personal_info',
'personal_details',
'product_selection_screen',
'additional_info',
'review_confirm_screen',
],
'flow 2': [
'aadhar_verification_screen',
],
},
child: MaterialApp.router(
routerConfig: appRouter,
),
);
}
}
How Flow-Based Activation Works
-
Flow Detection: When a user navigates to a route, the SDK automatically detects which flow the route belongs to based on your
enabledRoutesconfiguration. -
API Key Selection: The SDK retrieves the API key that was saved for the detected flow during initialization.
-
Flow Switching: When switching between flows (or navigating outside any flow), the EmbedWidget automatically:
- Ends any active calls
- Re-initializes with the new flow's API key when entering a new flow
-
Route Matching: Routes are matched using the
routeMatchModesetting (default:RouteMatchMode.exact).
Route Configuration Options
enabledRoutes:Map<String, List<String>>- Flow name to list of route names mapping- Key: Flow identifier (e.g.,
'flow 1','flow 2') - Value: List of route names that belong to that flow
- Key: Flow identifier (e.g.,
showOnAllRoutes: Show EmbedWidget on all routes (default:false)disabledRoutes: List of route names where EmbedWidget should be disabledrouteMatchMode: How to match routes:RouteMatchMode.exact- Exact match (default)RouteMatchMode.startsWith- Route starts with patternRouteMatchMode.contains- Route contains pattern
Optional Parameters #
apiKey(optional): Your Revrag AI API key for authentication (can be set globally viaembedInitialize()withflowName)embedUrl(optional): Custom base URL for API endpoints (can be set globally viaembedInitialize())showEmbedWidget(default:true): Controls whether the embedded widget is visibleenabledRoutes(optional):Map<String, List<String>>- Flow name to route names mapping for flow-based activationshowOnAllRoutes(default:false): Show EmbedWidget on all routesdisabledRoutes(optional): List of route names where EmbedWidget should be disabledrouteMatchMode(default:RouteMatchMode.exact): How to match routesrightPadding(optional):double- Right padding in pixels from the screen edge for the Embed Widget's position. When provided along withbottomPadding, the Embed Widget will be positioned from the right and bottom edges instead of using the default position.bottomPadding(optional):double- Bottom padding in pixels from the screen edge for the Embed Widget's position. When provided along withrightPadding, the Embed Widget will be positioned from the right and bottom edges instead of using the default position.onPermissionStatusChanged(optional): A callback function that receives a boolean indicating whether microphone permission is denied or permanently denied. If the microphone permission is not given, the initial permission request is prompted automatically when the call is started. If the user denies it, the result can be handled accordingly. Called when a call is initiated:true- Microphone permission was grantedfalse- Microphone permission was denied or permanently denied
Customizing Embed Widget's Position
You can customize the initial position of the Embed Widget using rightPadding and bottomPadding parameters. These parameters allow you to position the Embed Widget from the right and bottom edges of the screen:
EmbedWidget(
showEmbedWidget: true,
rightPadding: 20.0, // 20px from right edge
bottomPadding: 100.0, // 100px from bottom edge
enabledRoutes: {
'flow 1': ['home', 'product_selection_screen'],
},
child: MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()],
home: MyHomePage(),
),
)
Using onPermissionStatusChanged
The onPermissionStatusChanged callback allows you to handle microphone permission status changes when a user attempts to start a voice call. This is useful for showing custom UI feedback or handling permission denial gracefully:
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
// Global key for ScaffoldMessenger to show SnackBars from callbacks
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
@override
Widget build(BuildContext context) {
return EmbedWidget(
showEmbedWidget: true,
// Handle permission status changes
onPermissionStatusChanged: (bool isGranted) {
if (isGranted) {
// Permission granted - call can proceed
print('Microphone permission granted');
// You can show a success message or update UI here
} else {
// Permission denied - handle accordingly
print('Microphone permission denied');
// You can show an error message or guide user to settings
scaffoldMessengerKey.currentState?.showSnackBar(
const SnackBar(
content: Text('Microphone permission is required for voice calls. Please enable it in settings and re-open the app.'),
duration: Duration(seconds: 3),
),
);
}
},
enabledRoutes: {
'flow 1': ['home', 'product_selection_screen'],
},
child: MaterialApp(
scaffoldMessengerKey: scaffoldMessengerKey,
navigatorObservers: [EmbedNavigatorObserver()],
home: MyHomePage(),
),
);
}
}
When is it called?
- The callback is triggered when a user attempts to start a call
- It's called after the permission request dialog is shown (if needed)
- It's called before the call connection is established
Examples #
Basic Integration #
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';
void main() {
// Initialize the SDK
embedInitialize('your-api-key-here');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return EmbedWidget(
showEmbedWidget: true,
child: MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()], // Required for route tracking
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
// Send USER_DATA event to activate EmbedWidget
// This should typically be called after user authentication
embedEvent(
EventKeys.USER_DATA,
UserEventPayload(
app_user_id: 'user_12345', // Replace with actual user ID
data: {
'name': 'John Doe',
'email': 'john@example.com',
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My App')),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Welcome to My App!'),
SizedBox(height: 20),
Text('The embedded agent is monitoring this screen.'),
],
),
),
);
}
}
Form with Monitoring #
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';
void main() {
// Initialize the SDK
embedInitialize('your-api-key-here');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return EmbedWidget(
showEmbedWidget: true,
child: MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()], // Required for route tracking
home: const MyFormPage(),
),
);
}
}
class MyFormPage extends StatefulWidget {
const MyFormPage({super.key});
@override
State<MyFormPage> createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
@override
void initState() {
super.initState();
// Send USER_DATA event to activate EmbedWidget
// This should typically be called after user authentication
embedEvent(
EventKeys.USER_DATA,
UserEventPayload(
app_user_id: 'user_12345', // Replace with actual user ID
data: {
'name': 'John Doe',
'email': 'john@example.com',
},
),
);
// Send SCREEN_STATE event for additional context
embedEvent(
EventKeys.SCREEN_STATE,
ScreenEventPayload(
screen: 'form_page',
data: {
'form_type': 'user_registration',
'step': 'personal_info',
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My Form')),
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Form is valid - EmbedWidget will track the submission
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Form submitted successfully!')),
);
}
},
child: const Text('Submit'),
),
],
),
),
),
);
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
}
Flow-Based Configuration #
Configure the EmbedWidget with flow-based activation to organize routes into flows and use different API keys:
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';
void main() {
// Initialize API keys for different flows
embedInitialize('api-key-for-onboarding-flow', flowName: 'onboarding');
embedInitialize('api-key-for-checkout-flow', flowName: 'checkout');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return EmbedWidget(
// Configure flows with their associated routes
enabledRoutes: {
'onboarding': [
'welcome',
'personal-info',
'personal-details',
'additional-info'
],
'checkout': [
'product-selection',
'payment',
'review-confirm'
],
},
child: MaterialApp(
navigatorObservers: [EmbedNavigatorObserver()], // Required for route tracking
initialRoute: '/',
routes: {
'/': (context) => const SplashScreen(),
'welcome': (context) => const WelcomeScreen(),
'personal-info': (context) => const PersonalInfoScreen(),
'personal-details': (context) => const PersonalDetailsScreen(),
'product-selection': (context) => const ProductSelectionScreen(),
'additional-info': (context) => const AdditionalInfoScreen(),
},
),
);
}
}
Flow Behavior
- Within a Flow: When navigating between routes in the same flow, the EmbedWidget remains active and uses the same API key.
- Between Flows: When switching from one flow to another, the EmbedWidget:
- Automatically ends any active calls
- Cleans up resources
- Re-initializes with the new flow's API key
- Outside Flows: When navigating to routes not in any configured flow, the EmbedWidget is hidden and cleaned up.
Custom Router Integration #
An Example to show Full integration with Custom Routers we can take example of GoRouter for declarative navigation:
import 'package:go_router/go_router.dart';
import 'package:embed_flutter/embed_flutter.dart';
final appRouter = GoRouter(
initialLocation: '/splash',
routes: [
GoRoute(
path: '/splash',
name: 'splash_screen',
builder: (context, state) => const SplashScreen(),
),
GoRoute(
path: '/welcome',
name: 'welcome_screen',
builder: (context, state) => const WelcomeScreen(),
),
GoRoute(
path: '/personal-info',
name: 'personal_info',
builder: (context, state) => const PersonalInfoScreen(),
),
// ... more routes
],
// Add EmbedNavigatorObserver to the list of observers
observers: [
EmbedNavigatorObserver(),
// ... other observers
],
);
void main() {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
widgetsBinding = EmbedWidgetsBinding();
// Initialize API keys for different flows
embedInitialize('api-key-for-flow-1', flowName: 'flow 1');
embedInitialize('api-key-for-flow-2', flowName: 'flow 2');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return EmbedWidget(
enabledRoutes: {
'flow 1': ['welcome_screen', 'personal_info', 'product_selection_screen'],
'flow 2': ['checkout', 'payment'],
},
child: MaterialApp.router(
routerConfig: appRouter,
),
);
}
}
Configuration #
Custom Base URL #
You can set a custom base URL globally during initialization:
void main() {
embedInitialize(
'your-api-key',
embedUrl: 'https://your-custom-domain.com',
);
runApp(const MyApp());
}
Troubleshooting #
Common Issues #
-
Widget Not Visible
- Check
showEmbedWidgetparameter - Verify API key is valid
- Check network connectivity
- Check
-
Voice Call Issues / Microphone Permission Not Being Requested
-
Ensure microphone permission is granted and not permanently denied.
-
Check device audio settings
-
iOS: Verify that
NSMicrophoneUsageDescriptionis added toios/Runner/Info.plist -
iOS: Ensure the Podfile includes the permission handler configuration. Add the following to your
ios/Podfileif it's missing:post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_MICROPHONE=1' end end endAfter adding this configuration, run:
cd ios pod install cd ..
-
-
Widget Tree Not Updating
- Check widget tree handler initialization
- Verify context is available
- Check for widget disposal issues
-
Flow-Based Activation Not Working
- Verify route names match exactly (case-sensitive)
- Check
EmbedNavigatorObserveris added to router observers - Ensure GoRouter is properly configured with named routes
- Verify
enabledRoutesmap contains correct flow names and route names - Ensure each flow has been initialized with
embedInitialize()andflowNameparameter - Check that API keys are saved correctly for each flow (check console logs)
- Verify the current route belongs to a configured flow
-
Button Click Detection Issues
- Ensure buttons have proper keys for identification
- Check if buttons are properly tracked in widget tree
- Verify hit testing permissions and context
- Test with different button types (ElevatedButton, TextButton, etc.)
-
EmbedWidget Not Appearing on Expected Screens
- Check
enabledRoutesconfiguration (flow-based map structure) - Check
disabledRoutesconfiguration - Verify route matching mode (
exactvsstartsWithvscontains) - Check if
showEmbedWidgetis set totrue - Verify the route belongs to a configured flow
- Check console logs for flow detection messages
- Check
-
Wrong API Key Being Used for a Flow
- Verify
embedInitialize()was called with the correctflowNamefor each flow - Check that API keys are saved correctly (check console logs for
LocalStorageHandler.saveAPIKey) - Ensure flow names in
enabledRoutesmatch theflowNameused inembedInitialize() - Check console logs for
EmbedConfigService.apiKeyto see which API key is being retrieved
- Verify
License #
This project is licensed under the Non-Commercial License License - see the LICENSE file for details.
Support #
- Documentation: https://docs.revrag.ai/embed/integration/flutter
- Email Support: contact@revrag.ai
- GitHub Issues: https://github.com/RevRag-ai/embed-react-native/issues
Changelog #
See CHANGELOG.md for a complete list of changes and version history.