> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroruntime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Voice Agent with Flutter

> Connect a Zero Runtime voice AI agent to a native Flutter app so users can talk to it in real time using the Google Gemini Live API.

Zero Runtime lets you integrate AI agents with real-time voice interaction into your native Flutter application within minutes.

In this quickstart, you'll create an AI agent that joins a Flutter meeting room and interacts with users through voice using the Google Gemini Live API.

## Prerequisites

Before proceeding, ensure that your development environment meets the following requirements:

* A Zero Runtime account (create one from the [Zero Runtime Dashboard](https://app.zeroruntime.ai/)).
* The Flutter toolchain and Python 3.11+ installed on your device.
* A Google API key with Gemini Live API access.

<Note>
  You need a Zero Runtime account to generate a token and a Google API key for the Gemini Live API.
  Generate a token from the [Zero Runtime Dashboard](https://app.zeroruntime.ai/) and a Google API key from [Google AI Studio](https://aistudio.google.com/api-keys).
</Note>

## Project Structure

Your project structure should look like this:

<Tree>
  <Tree.Folder name="root" defaultOpen>
    <Tree.Folder name="android" />

    <Tree.Folder name="ios" />

    <Tree.Folder name="lib" defaultOpen>
      <Tree.File name="api_call.dart" />

      <Tree.File name="join_screen.dart" />

      <Tree.File name="main.dart" />

      <Tree.File name="meeting_controls.dart" />

      <Tree.File name="meeting_screen.dart" />

      <Tree.File name="participant_tile.dart" />
    </Tree.Folder>

    <Tree.Folder name="macos" />

    <Tree.Folder name="web" />

    <Tree.Folder name="windows" />

    <Tree.File name="agent-flutter.py" />

    <Tree.File name=".env" />
  </Tree.Folder>
</Tree>

You will be working on the following files:

* `join_screen.dart`: Responsible for the user interface to join a meeting.
* `meeting_screen.dart`: Displays the meeting interface and handles meeting logic.
* `api_call.dart`: Handles API calls for creating meetings.
* `agent-flutter.py`: The Python AI agent backend using the Google Gemini Live API.
* `.env`: Environment variables for the Python agent's API keys.

## 1. Flutter Frontend

<Steps>
  <Step title="Step 1: Create the Flutter app" icon="download">
    Create a new Flutter app using the following command:

    ```bash theme={null}
    flutter create zrt_ai_agent_flutter_app
    ```

    Install the RTC SDK using the following Flutter command. Make sure you are in your Flutter app directory before you run this command.

    ```bash theme={null}
    flutter pub add videosdk
    flutter pub add http
    ```
  </Step>

  <Step title="Step 2: Configure native platform permissions" icon="mobile">
    <Tabs>
      <Tab title="Android">
        Update `/android/app/src/main/AndroidManifest.xml` for the permissions used to implement the audio and video features.

        ```xml title="android/app/src/main/AndroidManifest.xml" theme={null}
        <uses-feature android:name="android.hardware.camera" />
        <uses-feature android:name="android.hardware.camera.autofocus" />
        <uses-permission android:name="android.permission.CAMERA" />
        <uses-permission android:name="android.permission.RECORD_AUDIO" />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
        <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
        <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
        <uses-permission android:name="android.permission.INTERNET"/>
        ```

        If necessary, in `build.gradle` you will need to increase the `minSdkVersion` of `defaultConfig` up to `23` (Flutter's generator defaults it to `16`).
      </Tab>

      <Tab title="iOS">
        Add the following entries which allow your app to access the camera and microphone to your `/ios/Runner/Info.plist` file:

        ```xml title="/ios/Runner/Info.plist" theme={null}
        <key>NSCameraUsageDescription</key>
        <string>$(PRODUCT_NAME) Camera Usage!</string>
        <key>NSMicrophoneUsageDescription</key>
        <string>$(PRODUCT_NAME) Microphone Usage!</string>
        ```

        Uncomment the following line to define a global platform for your project in `/ios/Podfile`:

        ```ruby title="/ios/Podfile" theme={null}
        platform :ios, '12.0'
        ```
      </Tab>

      <Tab title="macOS">
        Add the following entries to your `/macos/Runner/Info.plist` file, which allow your app to access the camera and microphone.

        ```xml title="/macos/Runner/Info.plist" theme={null}
        <key>NSCameraUsageDescription</key>
        <string>$(PRODUCT_NAME) Camera Usage!</string>
        <key>NSMicrophoneUsageDescription</key>
        <string>$(PRODUCT_NAME) Microphone Usage!</string>
        ```

        Add the following entries to your `/macos/Runner/DebugProfile.entitlements` file, which allow your app to access the camera, microphone, and open outgoing network connections.

        ```xml title="/macos/Runner/DebugProfile.entitlements" theme={null}
        <key>com.apple.security.network.client</key>
        <true/>
        <key>com.apple.security.device.camera</key>
        <true/>
        <key>com.apple.security.device.microphone</key>
        <true/>
        ```

        Add the following entries to your `/macos/Runner/Release.entitlements` file, which allow your app to access the camera, microphone, and open outgoing network connections.

        ```xml title="/macos/Runner/Release.entitlements" theme={null}
        <key>com.apple.security.network.server</key>
        <true/>
        <key>com.apple.security.network.client</key>
        <true/>
        <key>com.apple.security.device.camera</key>
        <true/>
        <key>com.apple.security.device.microphone</key>
        <true/>
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Step 3: Configure environment and credentials" icon="key">
    Create a meeting room using the Zero Runtime API:

    ```bash theme={null}
    curl -X POST https://api.videosdk.live/v2/rooms \
      -H "Authorization: YOUR_JWT_TOKEN_HERE" \
      -H "Content-Type: application/json"
    ```

    Copy the `roomId` from the response and configure it in `lib/join_screen.dart` and `lib/api_call.dart`.

    ```dart title="lib/api_call.dart" theme={null}
    import 'dart:convert';
    import 'package:http/http.dart' as http;

    //Auth token we will use to generate a meeting and connect to it
    const token =
        'YOUR_ZRT_AUTH_TOKEN';

    // API call to create meeting
    Future<String> createMeeting() async {
      final http.Response httpResponse = await http.post(
        Uri.parse('https://api.videosdk.live/v2/rooms'),
        headers: {'Authorization': token},
      );

      //Destructuring the roomId from the response
      return json.decode(httpResponse.body)['roomId'];
    }
    ```

    ```dart title="lib/join_screen.dart" theme={null}
    import 'package:flutter/material.dart';
    import 'api_call.dart';
    import 'meeting_screen.dart';

    class JoinScreen extends StatelessWidget {
      final _meetingIdController = TextEditingController();

      JoinScreen({super.key});

      void onCreateButtonPressed(BuildContext context) async {
        // call api to create meeting and navigate to MeetingScreen with meetingId,token
        await createMeeting().then((meetingId) {
          if (!context.mounted) return;
          Navigator.of(context).push(
            MaterialPageRoute(
              builder:
                  (context) => MeetingScreen(meetingId: meetingId, token: token),
            ),
          );
        });
      }

      void onJoinButtonPressed(BuildContext context) {
        // check meeting id is not null or invaild
        // if meeting id is vaild then navigate to MeetingScreen with meetingId,token
        Navigator.of(context).push(
          MaterialPageRoute(
            builder:
                (context) =>
                    MeetingScreen(meetingId: "YOUR_MEETING_ID", token: token),
          ),
        );
      }

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Zero Runtime QuickStart')),
          body: Padding(
            padding: const EdgeInsets.all(12.0),
            child: Center(
              child: ElevatedButton(
                onPressed: () => onJoinButtonPressed(context),
                child: const Text('Join Meeting'),
              ),
            ),
          ),
        );
      }
    }
    ```
  </Step>

  <Step title="Step 4: Design the user interface" icon="window">
    Create the main `MeetingScreen` widget with audio-only interaction in `lib/meeting_screen.dart`:

    ```dart title="lib/meeting_screen.dart" theme={null}
    import 'package:flutter/material.dart';
    import 'package:videosdk/videosdk.dart';
    import 'participant_tile.dart';
    import 'meeting_controls.dart';

    class MeetingScreen extends StatefulWidget {
      final String meetingId;
      final String token;

      const MeetingScreen({
        super.key,
        required this.meetingId,
        required this.token,
      });

      @override
      State<MeetingScreen> createState() => _MeetingScreenState();
    }

    class _MeetingScreenState extends State<MeetingScreen> {
      late Room _room;
      var micEnabled = true;
      var camEnabled = true;

      Map<String, Participant> participants = {};

      @override
      void initState() {
        // create room
        _room = VideoSDK.createRoom(
          roomId: widget.meetingId,
          token: widget.token,
          displayName: "John Doe",
          micEnabled: micEnabled,
          camEnabled: false,
          defaultCameraIndex:
              1, // Index of MediaDevices will be used to set default camera
        );

        setMeetingEventListener();

        // Join room
        _room.join();

        super.initState();
      }

      // listening to meeting events
      void setMeetingEventListener() {
        _room.on(Events.roomJoined, () {
          setState(() {
            participants.putIfAbsent(
              _room.localParticipant.id,
              () => _room.localParticipant,
            );
          });
        });

        _room.on(Events.participantJoined, (Participant participant) {
          setState(
            () => participants.putIfAbsent(participant.id, () => participant),
          );
        });

        _room.on(Events.participantLeft, (String participantId) {
          if (participants.containsKey(participantId)) {
            setState(() => participants.remove(participantId));
          }
        });

        _room.on(Events.roomLeft, () {
          participants.clear();
          Navigator.popUntil(context, ModalRoute.withName('/'));
        });
      }

      // onbackButton pressed leave the room
      Future<bool> _onWillPop() async {
        _room.leave();
        return true;
      }

      @override
      Widget build(BuildContext context) {
        return WillPopScope(
          onWillPop: () => _onWillPop(),
          child: Scaffold(
            appBar: AppBar(title: const Text('Zero Runtime QuickStart')),
            body: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: [
                  Text(widget.meetingId),
                  //render all participant
                  Expanded(
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: GridView.builder(
                        gridDelegate:
                            const SliverGridDelegateWithFixedCrossAxisCount(
                              crossAxisCount: 2,
                              crossAxisSpacing: 10,
                              mainAxisSpacing: 10,
                              mainAxisExtent: 300,
                            ),
                        itemBuilder: (context, index) {
                          return ParticipantTile(
                            key: Key(participants.values.elementAt(index).id),
                            participant: participants.values.elementAt(index),
                          );
                        },
                        itemCount: participants.length,
                      ),
                    ),
                  ),
                  MeetingControls(
                    onToggleMicButtonPressed: () {
                      micEnabled ? _room.muteMic() : _room.unmuteMic();
                      micEnabled = !micEnabled;
                    },
                    onLeaveButtonPressed: () => _room.leave(),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    ```

    ```dart title="lib/participant_tile.dart" theme={null}
    import 'package:flutter/material.dart';
    import 'package:videosdk/videosdk.dart';

    class ParticipantTile extends StatefulWidget {
      final Participant participant;
      const ParticipantTile({super.key, required this.participant});

      @override
      State<ParticipantTile> createState() => _ParticipantTileState();
    }

    class _ParticipantTileState extends State<ParticipantTile> {
      var pariticpantName;
      @override
      void initState() {
        pariticpantName = widget.participant.displayName;

        super.initState();
      }

      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
            color: Colors.grey.shade800,
            child: Center(
              child: Text(
                '$pariticpantName',
                style: TextStyle(color: Colors.white),
              ),
            ),
          ),
        );
      }
    }
    ```

    ```dart title="lib/meeting_controls.dart" theme={null}
    import 'package:flutter/material.dart';

    class MeetingControls extends StatelessWidget {
      final void Function() onToggleMicButtonPressed;
      final void Function() onLeaveButtonPressed;

      const MeetingControls({
        super.key,
        required this.onToggleMicButtonPressed,
        required this.onLeaveButtonPressed,
      });

      @override
      Widget build(BuildContext context) {
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(
              onPressed: onLeaveButtonPressed,
              child: const Text('Leave'),
            ),
            ElevatedButton(
              onPressed: onToggleMicButtonPressed,
              child: const Text('Toggle Mic'),
            ),
          ],
        );
      }
    }
    ```
  </Step>
</Steps>

## 2. Python AI Agent

<Steps>
  <Step title="Step 1: Setup the project" icon="download">
    Use Python 3.11 or higher, then install the Zero Runtime Agents SDK. Create and activate a virtual environment, then install `zrt` and `python-dotenv`. Every provider plugin ships with `zrt`.

    <CodeGroup>
      ```bash uv theme={null}
      uv venv --python 3.11
      source .venv/bin/activate     # macOS/Linux
      .\.venv\Scripts\activate      # Windows
      ```

      ```bash pip theme={null}
      python3.11 -m venv venv
      source venv/bin/activate      # macOS/Linux
      .\venv\Scripts\activate       # Windows
      ```
    </CodeGroup>

    <Tip>
      New to uv? See the [uv install guide](https://docs.astral.sh/uv/getting-started/installation/).
    </Tip>

    <CodeGroup>
      ```bash uv theme={null}
      uv pip install zrt python-dotenv
      ```

      ```bash pip theme={null}
      pip install zrt python-dotenv
      ```
    </CodeGroup>
  </Step>

  <Step title="Step 2: Configure environment variables" icon="key">
    Create a `.env` file in your project root to store your keys and token securely for the Python agent:

    ```shell title=".env" theme={null}
    # Google API Key for the Gemini Live API
    GOOGLE_API_KEY=your_google_api_key_here

    # Option A: pre-generated ZRT auth token
    ZRT_AUTH_TOKEN=your_zrt_auth_token_here

    # Option B: SDK auto-generates the token (omit ZRT_AUTH_TOKEN)
    # ZRT_API_KEY=your_zrt_api_key
    # ZRT_SECRET_KEY=your_zrt_secret_key
    ```
  </Step>

  <Step title="Step 3: Create the AI agent backend" icon="robot">
    Create the Python AI agent (`agent-flutter.py`) that joins the same meeting room and interacts with users through voice.

    ```python title="agent-flutter.py" theme={null}
    import os
    import zrt
    from zrt import Agent, Pipeline, Room
    from zrt.plugins import GeminiRealtime, GeminiLiveConfig
    from zrt.inference import AICousticsDenoise
    from dotenv import load_dotenv

    load_dotenv()

    AGENT_ID = "flutter-voice-agent"

    pipeline = Pipeline(
        # Realtime speech-to-speech model goes in the pipeline's llm slot -
        # no separate STT or TTS needed.
        llm=GeminiRealtime(
            # When GOOGLE_API_KEY is set in .env you can omit api_key.
            api_key=os.getenv("GOOGLE_API_KEY"),
            config=GeminiLiveConfig(model="gemini-3.1-flash-live-preview", voice="Leda", response_modalities=["AUDIO"]),
        ),
        denoise=AICousticsDenoise(model_id="quail-vf-2.2-l-16khz"),
    )


    class MyVoiceAgent(Agent):
        def __init__(self) -> None:
            super().__init__(
                name="MyVoiceAgent",
                agent_id=AGENT_ID,
                instructions="You are a high-energy game-show host guiding the caller to guess a secret number from 1 to 100 to win 1,000,000$.",
                pipeline=pipeline,
            )

        async def on_enter(self) -> None:
            await self.session.say(
                "Welcome to the Zero Runtime AI Agent game show! I'm your host, and we're about to play for 1,000,000$. Are you ready to play?"
            )

        async def on_exit(self) -> None:
            await self.session.say("Goodbye!")


    def invoke_agent() -> None:
        # Join the same room the Flutter app created (its meeting ID).
        zrt.invoke(AGENT_ID, room=Room(room_id="YOUR_MEETING_ID"))


    if __name__ == "__main__":
        zrt.serve(MyVoiceAgent, on_ready=invoke_agent)
    ```
  </Step>
</Steps>

## 3. Run the Application

<Steps>
  <Step title="Step 1: Run the Flutter app" icon="play">
    Once you have completed all the steps above, start your Flutter application:

    ```bash theme={null}
    flutter run
    ```
  </Step>

  <Step title="Step 2: Run the AI agent" icon="terminal">
    Open a new terminal, activate your virtual environment, and run the Python agent. It reads `GOOGLE_API_KEY` from the `.env` file.

    <CodeGroup>
      ```bash uv theme={null}
      uv run python agent-flutter.py
      ```

      ```bash pip theme={null}
      python agent-flutter.py
      ```
    </CodeGroup>
  </Step>

  <Step title="Step 3: Connect and interact" icon="microphone">
    1. **Join the meeting from the Flutter app:**
       * Tap the **Join Meeting** button.
       * Allow microphone permissions when prompted.

    2. **Agent connection:**
       * Once you join, the Python agent detects your participation.
       * You should see "Participant joined" in the terminal.
       * The AI agent greets you and initiates the game.

    3. **Start playing:**
       * The agent guides you through a number guessing game (1–100).
       * Use your microphone to interact with the AI host.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="'Waiting for participant...' but no connection" icon="plug">
    * Ensure both the Flutter app and the agent are running.
    * Check that the room ID matches in both `lib/join_screen.dart` and `agent-flutter.py`.
    * Verify your Zero Runtime token is valid.
  </Accordion>

  <Accordion title="Audio not working" icon="volume-high">
    * Check device permissions for microphone access.
    * Ensure your Google API key has Gemini Live API access enabled.
  </Accordion>

  <Accordion title="Agent not responding" icon="robot">
    * Verify your `GOOGLE_API_KEY` is set in the `.env` file.
    * Check that the Gemini Live API is enabled in your Google Cloud Console.
  </Accordion>

  <Accordion title="Flutter build issues" icon="hammer">
    * Ensure your Flutter version is compatible.
    * Try cleaning the build: `flutter clean`.
    * Delete `pubspec.lock` and run `flutter pub get`.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Quickstart Example" icon="github" href="https://github.com/ZeroRuntimeAI/zrt-python-sdk-examples">
    Complete working example with source code.
  </Card>

  <Card title="Build Your First Voice Agent" icon="microphone" href="/quickstarts/build-your-first-voice-agent">
    Learn the Cascade and Realtime pipelines in depth.
  </Card>
</CardGroup>
