Skip to main content

Embed Flutter SDK Integration Guide

Overview

The Embed Flutter SDK is a powerful voice-enabled AI agent library that provides real-time widget tree monitoring for screen context fetching and real-time voice communication. This comprehensive guide will walk you through the complete integration process from installation to deployment.

Prerequisites

  • Flutter 3.0+
  • Dart 2.17+
  • Platform specific:
    • iOS 12.0+
    • Android API 21+
  • Device with microphone capabilities
This SDK requires proper setup of device permissions and real-time communication dependencies for voice functionality.

Installation

Add the following dependency to your pubspec.yaml:
dependencies:
  embed_flutter: ^0.0.4
Then run:
flutter pub get

Android Configuration

CRITICAL: Add all required permissions to your Android manifest. Missing permissions will cause the voice functionality to fail.
Add the following permissions to your android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 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" />

    <application
        android:name=".MainApplication"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:allowBackup="false"
        android:theme="@style/AppTheme"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:hardwareAccelerated="true">

        <!-- Your activities and other components -->

    </application>
</manifest>

iOS Configuration

CRITICAL: Add the following permissions to your ios/Runner/Info.plist. Missing NSMicrophoneUsageDescription will cause the app to crash when accessing the microphone.
Add the following permissions to your ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for voice communication with AI agent</string>

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    <key>NSAllowsLocalNetworking</key>
    <true/>
</dict>

SDK Initialization

embedInitialize Function

CRITICAL: Always initialize the SDK before using any EmbedWidget. Missing initialization will cause the widget to fail.
Before using any EmbedWidget, you must initialize the configuration once in your main.dart:
void main() {
  embedInitialize(
    'your-api-key-here',
    embedUrl: 'https://embed.revrag.ai', // Optional
  );
  runApp(const MyApp());
}

Configuration Options

PropertyTypeRequiredDescription
apiKeyStringYour Embed API key
embedUrlStringCustom base URL for API endpoints

App Setup

1. Required: Navigator Observer

CRITICAL: 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
)

2. Wrap Main App with EmbedWidget

Wrap your main app content with EmbedWidget:
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';

void main() {
  embedInitialize('your-api-key-here');
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      child: MaterialApp(
        navigatorObservers: [EmbedNavigatorObserver()], // Required for route tracking
        home: MyHomePage(),
      ),
    );
  }
}

Event System

The SDK provides a powerful event system to send user context and application state to the AI agent. There are two main event types that must be used correctly:

Available Events

Sending Events (EventKeys)

import 'package:embed_flutter/embed_flutter.dart';

// Available event keys for sending data:
EventKeys.USER_DATA    // User identity and profile information
EventKeys.SCREEN_STATE // Application state and navigation context

Event Usage Rules

CRITICAL REQUIREMENTS:
  1. USER_DATA event MUST be sent first before any other events or EmbedWidget functionality
  2. USER_DATA must include app_user_id for user identification
  3. SCREEN_STATE events can only be sent after USER_DATA is established
  4. EmbedWidget will not be fully functional until USER_DATA event is sent

Required: USER_DATA Event

To activate the EmbedWidget and enable voice functionality, you must send the USER_DATA event with a user ID. This is typically done after user authentication:
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',
      'plan': 'premium',
    },
  ),
);

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',
    },
  ),
);

Event Flow Example

// Example event flow for sending data
try {
  // Step 1: Send user data first (required)
  await embedEvent(
    EventKeys.USER_DATA,
    UserEventPayload(
      app_user_id: 'user123',
      data: {
        'name': 'John Doe',
        'first_name': 'John',
        'last_name': 'Doe',
        'email': 'john@example.com',
      },
    ),
  );

  // Step 2: Send screen context data (optional)
  await embedEvent(
    EventKeys.SCREEN_STATE,
    ScreenEventPayload(
      screen: 'profile',
      data: {
        'plan': 'premium',
        'section': 'account_settings',
      },
    ),
  );
} catch (error) {
  print('Event error: $error');
}

Event Summary Table

EventPurposeRequiredWhen to Use
USER_DATAInitialize user context and activate EmbedWidgetRequiredAfter user authentication or when user ID is available
SCREEN_STATEProvide screen context and navigation infoOptionalWhen navigating between screens or when screen context changes

Route-Based Activation

The SDK supports intelligent route-based activation, allowing you to control exactly which screens should display the EmbedWidget:
void main() {
  embedInitialize('your-api-key-here');
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      // Activate only on specific routes
      enabledRoutes: ['welcome_screen', 'personal_info', 'product_selection'],

      // Optional: Show on all routes except disabled ones
      // showOnAllRoutes: true,
      // disabledRoutes: ['splash_screen', 'success_screen'],

      // Optional: Route matching mode (exact or startsWith)
      // routeMatchMode: RouteMatchMode.exact,

      child: MaterialApp.router(
        routerConfig: appRouter,
      ),
    );
  }
}

Route Configuration Options

  • enabledRoutes: List of route names where EmbedWidget should be active
  • showOnAllRoutes: Show EmbedWidget on all routes (default: false)
  • disabledRoutes: List of route names where EmbedWidget should be disabled
  • routeMatchMode: How to match routes (RouteMatchMode.exact, RouteMatchMode.startsWith, or RouteMatchMode.contains)

Required Parameters

  • child (required): The main content of your app

Optional Parameters

  • apiKey (optional): Your Revrag AI API key for authentication (can be set globally via embedInitialize())
  • embedUrl (optional): Custom base URL for API endpoints (can be set globally via embedInitialize())
  • showEmbedWidget (default: true): Controls whether the embedded widget is visible
  • enabledRoutes (optional): List of route names where EmbedWidget should be active
  • showOnAllRoutes (default: false): Show EmbedWidget on all routes
  • disabledRoutes (optional): List of route names where EmbedWidget should be disabled
  • routeMatchMode (default: RouteMatchMode.exact): How to match routes

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(
      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(
      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();
  }
}

Route-Based Configuration

Configure the EmbedWidget to activate only on specific screens:
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      // Only show on user-facing screens, not on splash or success
      enabledRoutes: [
        'welcome',
        'personal-info',
        'personal-details',
        'product-selection',
        'additional-info'
      ],
      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(),
        },
      ),
    );
  }
}

Custom Router Integration

Full integration with 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() {
  embedInitialize('your-api-key-here');
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      enabledRoutes: ['welcome_screen', 'personal_info', 'product_selection_screen'],
      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

This error occurs when you try to use EmbedWidget functionality before sending USER_DATA:
  1. ✅ Send USER_DATA event first with app_user_id
  2. ✅ Wait for the event to complete before using other features
  3. ✅ Only use EmbedWidget after USER_DATA is sent
// ❌ Wrong order
EmbedWidget(child: MyApp()); // Error! No USER_DATA sent

// ✅ Correct order
class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    // Send USER_DATA first
    embedEvent(
      EventKeys.USER_DATA,
      UserEventPayload(app_user_id: 'user123'),
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return EmbedWidget(child: MyContent()); // Works!
  }
}
Check these requirements:
  1. ✅ SDK initialized successfully (embedInitialize() called)
  2. ✅ Valid API key provided
  3. ✅ USER_DATA event sent with valid app_user_id
  4. ✅ Required permissions granted
  5. ✅ Network connectivity available
  6. showEmbedWidget parameter set to true
// ✅ Correct implementation
void main() {
  embedInitialize('valid-api-key');
  runApp(const MyApp());
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    embedEvent(EventKeys.USER_DATA, UserEventPayload(app_user_id: 'user123'));
  }
  
  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true, // Ensure this is true
      child: YourApp(),
    );
  }
}
This crash occurs when the app tries to access the microphone without proper permission:
  1. ✅ Open ios/Runner/Info.plist
  2. ✅ Add NSMicrophoneUsageDescription with a user-friendly description:
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for voice communication with AI agent</string>
  1. ✅ Clean and rebuild: flutter clean && flutter pub get && flutter run
This issue is usually related to device permissions or network configuration:
  1. ✅ Ensure all Android/iOS permissions are added to manifest/Info.plist
  2. ✅ Check device microphone permissions in Settings
  3. ✅ Verify network connectivity
  4. ✅ Test on physical device (not simulator/emulator)
  5. ✅ Check if USER_DATA event was sent successfully
// Check microphone permissions programmatically
import 'package:permission_handler/permission_handler.dart';

final status = await Permission.microphone.status;
if (!status.isGranted) {
  await Permission.microphone.request();
}
Verify route tracking is properly configured:
  1. ✅ Verify route names match exactly (case-sensitive)
  2. ✅ Check EmbedNavigatorObserver is added to navigator observers
  3. ✅ Ensure GoRouter is properly configured with named routes
  4. ✅ Verify enabledRoutes list contains correct route names
// ✅ Correct GoRouter setup
final router = GoRouter(
  observers: [EmbedNavigatorObserver()], // Required!
  routes: [
    GoRoute(
      path: '/welcome',
      name: 'welcome_screen', // This name should match enabledRoutes
      builder: (context, state) => WelcomeScreen(),
    ),
  ],
);

// ✅ Correct EmbedWidget setup
EmbedWidget(
  enabledRoutes: ['welcome_screen'], // Must match route names exactly
  child: MaterialApp.router(routerConfig: router),
)
Verify widget tree monitoring is working:
  1. ✅ Verify EmbedWidget wraps your main content
  2. ✅ Check that widget context is available
  3. ✅ Ensure proper widget disposal
  4. ✅ Test with simple widget hierarchy first
  5. ✅ Verify USER_DATA event was sent
// ✅ Correct widget structure
EmbedWidget(
  child: Scaffold(
    body: YourContent(), // This will be monitored
  ),
)
Common issues with event sending:
  1. Ensure USER_DATA is sent first:
// ✅ Correct event order
await embedEvent(EventKeys.USER_DATA, UserEventPayload(app_user_id: 'user123'));
await embedEvent(EventKeys.SCREEN_STATE, ScreenEventPayload(screen: 'home'));
  1. Check network connectivity and API endpoint
  2. Verify API key is valid and has proper permissions
  3. Handle async operations properly with try-catch:
try {
  await embedEvent(EventKeys.USER_DATA, payload);
} catch (error) {
  print('Event error: $error');
}
Check widget tree optimization:
  1. ✅ Avoid deeply nested widget trees
  2. ✅ Use const constructors where possible
  3. ✅ Check for memory leaks in event listeners
  4. ✅ Monitor app performance metrics
  5. ✅ Limit frequency of SCREEN_STATE events
// Optimize widget performance
const EmbedWidget(
  child: const Scaffold(
    body: const YourOptimizedContent(),
  ),
)

Best Practices

Event Optimization:
  • Send USER_DATA event as early as possible in the app lifecycle
  • Debounce frequent SCREEN_STATE events to avoid overwhelming the API
  • Handle offline scenarios with retry logic
  • Store critical events locally when network is unavailable
Initialization Strategy:
  • Show loading states during SDK initialization
  • Handle initialization errors gracefully
  • Only render EmbedWidget after successful initialization and USER_DATA event
  • Test initialization on various network conditions
Performance Optimization:
  • Use const constructors to reduce widget rebuilds
  • Avoid deeply nested widget hierarchies
  • Implement proper widget disposal in dispose() methods
  • Monitor memory usage and optimize accordingly
  • Limit widget tree depth for better monitoring performance
Security & Privacy:
  • Store API keys securely using flutter_secure_storage
  • Request only necessary permissions with clear explanations
  • Validate all user inputs before processing
  • Use HTTPS endpoints in production
  • Never expose API keys in client-side code
User Experience:
  • Show loading states during SDK initialization
  • Provide clear permission request explanations
  • Handle offline scenarios gracefully
  • Test voice functionality on various devices and network conditions
  • Provide fallback UI when voice features are unavailable

Support

For additional help:
Last Updated: September 2025