Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Integrate Video Chat Features in FlutterFlow Using Agora

Add video chat to FlutterFlow using the Agora RTC Engine package via a Custom Widget. Create an Agora project to get an App ID, deploy a Cloud Function to generate temporary channel tokens using your App Certificate, and build a Custom Widget with AgoraVideoView for local and remote video streams. Add controls for mute, camera toggle, and end call. Never expose the App Certificate in client code — only the App ID is safe to use client-side.

What you'll learn

  • How to create an Agora project and configure the agora_rtc_engine package in FlutterFlow
  • How to build a Custom Widget displaying local and remote video using AgoraVideoView
  • How to generate secure Agora channel tokens from a Firebase Cloud Function
  • How to add camera controls, audio mute, speaker toggle, and end-call functionality
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read90-120 minFlutterFlow Pro+ (Custom Widgets require paid plan)March 2026RapidDev Engineering Team
TL;DR

Add video chat to FlutterFlow using the Agora RTC Engine package via a Custom Widget. Create an Agora project to get an App ID, deploy a Cloud Function to generate temporary channel tokens using your App Certificate, and build a Custom Widget with AgoraVideoView for local and remote video streams. Add controls for mute, camera toggle, and end call. Never expose the App Certificate in client code — only the App ID is safe to use client-side.

Real-time video calling in FlutterFlow using Agora's managed WebRTC infrastructure

Agora.io is the most widely used video SDK for Flutter apps, providing managed global WebRTC infrastructure that handles signaling, codec negotiation, NAT traversal, and media routing. Integrating it into FlutterFlow requires a Custom Widget because video rendering uses platform-specific camera and display primitives that FlutterFlow's visual builder cannot configure directly. The Custom Widget creates an RTC Engine instance, joins a channel, and renders video tiles using AgoraVideoView. A Cloud Function generates short-lived tokens per channel so that only authorized users can join. For a 1-on-1 call, two participants join the same channel using a shared channel name; for group calls, the widget renders a dynamic grid of video tiles.

Prerequisites

  • A FlutterFlow Pro+ project (Custom Widgets require a paid plan)
  • An Agora.io account — free tier includes 10,000 monthly voice and video minutes
  • Cloud Functions enabled on Firebase (Blaze plan required for token generation)
  • Camera and microphone permissions configured in your app (FlutterFlow Settings → App Details → Permissions)

Step-by-step guide

1

Create an Agora project and add the package to FlutterFlow

Sign in to console.agora.io → Create a new project. Choose Testing mode initially (no token required) for development, then switch to Secured mode for production. Copy the App ID from the project dashboard — this is safe to use in client code. Copy the App Certificate — this must NEVER be in client code. In FlutterFlow, go to Custom Code → Pubspec Dependencies → Add Dependency. Add agora_rtc_engine with the latest stable version (check pub.dev for current version: as of Q1 2026, version 6.3.x is stable). Also add permission_handler for requesting camera/microphone permissions at runtime. Click Compile Code to verify the dependencies resolve.

Expected result: The agora_rtc_engine and permission_handler packages are added to your FlutterFlow project and compile successfully.

2

Create the token generation Cloud Function

Deploy a Cloud Function named generateAgoraToken. Install the Agora token library: npm install agora-access-token in your functions directory. The function accepts channelName and userId as parameters. It imports AgoraAccessToken, creates a RtcTokenBuilder, and builds a token with role Publisher, expiry of 3600 seconds (1 hour), and the App Certificate stored in Cloud Function environment: firebase functions:config:set agora.app_id='YOUR_APP_ID' agora.app_certificate='YOUR_CERTIFICATE'. Return the token and appId to the caller. FlutterFlow will call this function before joining any channel, ensuring tokens are always fresh.

generate_agora_token.js
1const functions = require('firebase-functions');
2const { RtcTokenBuilder, RtcRole } = require('agora-access-token');
3
4const APP_ID = functions.config().agora.app_id;
5const APP_CERTIFICATE = functions.config().agora.app_certificate;
6
7exports.generateAgoraToken = functions.https.onRequest((req, res) => {
8 res.set('Access-Control-Allow-Origin', '*');
9 if (req.method === 'OPTIONS') return res.status(204).send('');
10
11 const { channelName, userId } = req.query;
12 if (!channelName || !userId) {
13 return res.status(400).json({ error: 'channelName and userId required' });
14 }
15
16 const expirationTimeInSeconds = 3600;
17 const currentTimestamp = Math.floor(Date.now() / 1000);
18 const privilegeExpiredTs = currentTimestamp + expirationTimeInSeconds;
19
20 const token = RtcTokenBuilder.buildTokenWithUid(
21 APP_ID,
22 APP_CERTIFICATE,
23 channelName,
24 parseInt(userId) || 0,
25 RtcRole.PUBLISHER,
26 privilegeExpiredTs
27 );
28
29 res.json({ token, appId: APP_ID, channelName });
30});

Expected result: The Cloud Function returns a temporary Agora token valid for 1 hour for the specified channel. FlutterFlow uses this token when joining a call.

3

Build the VideoCallWidget Custom Widget

In FlutterFlow → Custom Code → Custom Widgets → Add Widget. Name it VideoCallWidget. Add parameters: channelName (String), token (String), appId (String), userId (Integer), isHost (Boolean). The widget builds using StatefulWidget. In initState, request camera and microphone permissions using permission_handler. Create the RtcEngine: await RtcEngine.createWithContext(RtcEngineContext(appId)). Enable video: await _engine.enableVideo(). Join the channel: await _engine.joinChannel(token: token, channelId: channelName, info: null, uid: userId). In the build method, return a Stack containing: a full-screen AgoraVideoView for the remote video, and a small AgoraVideoView in the bottom-right corner for the local video preview using RtcLocalView.SurfaceView(). Add event handlers for onUserJoined, onUserOffline, and onTokenPrivilegeWillExpire (to refresh the token from the Cloud Function).

Expected result: The VideoCallWidget displays local video in a corner preview and shows the remote participant's video full-screen when they join the channel.

4

Add call controls overlay on top of the video view

In the VideoCallWidget, add a Positioned widget at the bottom of the Stack containing a Row of control buttons. Each button is a CircleAvatar with an IconButton. Controls needed: (1) Mute audio toggle — calls _engine.muteLocalAudioStream(isMuted). Change microphone icon to mic_off when muted. (2) Toggle camera on/off — calls _engine.muteLocalVideoStream(isCameraOff). Replace local video with a grey placeholder when off. (3) Switch front/back camera — calls _engine.switchCamera(). (4) End call — calls _engine.leaveChannel(), disposes the engine, and calls the onCallEnded callback parameter to navigate back in FlutterFlow. Track state of each toggle using setState so button icons update visually.

Expected result: The video call screen shows mute, camera, flip camera, and end call buttons overlaid on the video. Each button reflects its current state visually.

5

Wire the complete call flow in FlutterFlow

Create a VideoCallPage with Page Parameters: channelName (String) and targetUserId (String). Add an On Page Load Action Flow: (1) Call the generateAgoraToken Cloud Function API passing channelName and current user UID. (2) Store the returned token in Page State tokenValue. (3) Store appId in Page State appIdValue. Once Page State is populated, show the VideoCallWidget bound to these values. Create a call-initiation Action Flow on your chat screen: generate a unique channelName (e.g., Firebase push ID), write a callNotification to Firestore with channelName and targetUserId, then navigate to VideoCallPage passing the channelName. On the other user's device, a real-time Backend Query on callNotifications detects the incoming call and shows an incoming call Bottom Sheet with Accept (navigate to VideoCallPage) and Decline buttons.

Expected result: User A taps Call, a channel is created and written to Firestore. User B sees an incoming call notification and can accept to join the same channel. Both see each other's video.

Complete working example

video_call_widget.dart
1import 'package:flutter/material.dart';
2import 'package:agora_rtc_engine/agora_rtc_engine.dart';
3import 'package:permission_handler/permission_handler.dart';
4
5class VideoCallWidget extends StatefulWidget {
6 final String channelName;
7 final String token;
8 final String appId;
9 final int userId;
10 final VoidCallback onCallEnded;
11
12 const VideoCallWidget({
13 Key? key,
14 required this.channelName,
15 required this.token,
16 required this.appId,
17 required this.userId,
18 required this.onCallEnded,
19 }) : super(key: key);
20
21 @override
22 State<VideoCallWidget> createState() => _VideoCallWidgetState();
23}
24
25class _VideoCallWidgetState extends State<VideoCallWidget> {
26 late RtcEngine _engine;
27 int? _remoteUid;
28 bool _isMuted = false;
29 bool _cameraOff = false;
30
31 @override
32 void initState() {
33 super.initState();
34 _initAgora();
35 }
36
37 Future<void> _initAgora() async {
38 await [Permission.microphone, Permission.camera].request();
39 _engine = createAgoraRtcEngine();
40 await _engine.initialize(RtcEngineContext(appId: widget.appId));
41 await _engine.enableVideo();
42
43 _engine.registerEventHandler(RtcEngineEventHandler(
44 onUserJoined: (_, uid, __) => setState(() => _remoteUid = uid),
45 onUserOffline: (_, uid, __) => setState(() => _remoteUid = null),
46 ));
47
48 await _engine.joinChannel(
49 token: widget.token,
50 channelId: widget.channelName,
51 uid: widget.userId,
52 options: const ChannelMediaOptions(),
53 );
54 }
55
56 @override
57 void dispose() {
58 _engine.leaveChannel();
59 _engine.release();
60 super.dispose();
61 }
62
63 @override
64 Widget build(BuildContext context) {
65 return Scaffold(
66 backgroundColor: Colors.black,
67 body: Stack(children: [
68 _remoteUid != null
69 ? AgoraVideoView(
70 controller: VideoViewController.remote(
71 rtcEngine: _engine,
72 canvas: VideoCanvas(uid: _remoteUid),
73 connection: RtcConnection(channelId: widget.channelName),
74 ))
75 : const Center(child: Text('Waiting...', style: TextStyle(color: Colors.white))),
76 Positioned(
77 bottom: 80, right: 16, width: 100, height: 150,
78 child: AgoraVideoView(
79 controller: VideoViewController(
80 rtcEngine: _engine,
81 canvas: const VideoCanvas(uid: 0),
82 )),
83 ),
84 Positioned(
85 bottom: 16, left: 0, right: 0,
86 child: Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
87 IconButton(icon: Icon(_isMuted ? Icons.mic_off : Icons.mic, color: Colors.white),
88 onPressed: () { _engine.muteLocalAudioStream(!_isMuted); setState(() => _isMuted = !_isMuted); }),
89 IconButton(icon: const Icon(Icons.call_end, color: Colors.red, size: 36),
90 onPressed: () { _engine.leaveChannel(); widget.onCallEnded(); }),
91 IconButton(icon: Icon(_cameraOff ? Icons.videocam_off : Icons.videocam, color: Colors.white),
92 onPressed: () { _engine.muteLocalVideoStream(!_cameraOff); setState(() => _cameraOff = !_cameraOff); }),
93 ]),
94 ),
95 ]),
96 );
97 }
98}

Common mistakes

Why it's a problem: Using the App Certificate directly in the FlutterFlow Custom Widget code instead of a Cloud Function

How to avoid: Store the App Certificate only in Cloud Function environment variables. The Custom Widget calls the Cloud Function to get a token; the Cloud Function uses the Certificate to generate it. Only the temporary token (valid 1 hour) touches client code.

Why it's a problem: Not disposing the RtcEngine when the widget is removed from the tree

How to avoid: Always call _engine.leaveChannel() and _engine.release() in the widget's dispose() method. Also handle the physical back button (WillPopScope or PopScope) to end the call before navigating away.

Why it's a problem: Not requesting camera and microphone permissions before initializing Agora

How to avoid: Request both Permission.camera and Permission.microphone using permission_handler before initializing the engine. Check the permission status first and show a custom explanation dialog before requesting if the user has not been asked before.

Best practices

  • Always use token authentication (Secured mode) in production — Testing mode (no token) allows anyone who guesses a channel name to join your calls
  • Store active call state in Firestore so participants can detect if the other person has dropped and show a 'Call ended' screen automatically
  • Add a ring/busy mechanism using Firestore real-time listeners before joining the channel — show incoming call UI and allow the callee to accept or decline
  • Implement token expiry handling by listening to the onTokenPrivilegeWillExpire event and refreshing the token from the Cloud Function before it expires
  • Test on physical devices only — camera and microphone permissions do not work correctly in web simulators or the FlutterFlow Run mode preview
  • Add a connection quality indicator using Agora's onNetworkQuality callback so users understand if the call quality issue is on their side or the other participant's
  • Handle app backgrounding — iOS suspends camera access when the app is in the background, so implement proper lifecycle handling to pause video when backgrounded

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am building a FlutterFlow app with video calling using the agora_rtc_engine Flutter package. Write a StatefulWidget Custom Widget in Dart that: (1) requests camera and microphone permissions, (2) initializes RtcEngine with an App ID, (3) joins a channel with a provided token and user ID, (4) displays remote video full-screen and local video in a corner overlay using AgoraVideoView, and (5) includes mute, camera toggle, and end-call buttons. Include proper engine disposal in the dispose method.

FlutterFlow Prompt

Create a video call page in my FlutterFlow app that: on page load calls a generateAgoraToken Cloud Function API to get a token for a channelName page parameter, then shows my VideoCallWidget custom widget with that token. When the call ends, navigate back to the previous page.

Frequently asked questions

How do I add video calling to a FlutterFlow app?

Use the agora_rtc_engine Flutter package added via FlutterFlow's Pubspec Dependencies. Build a Custom Widget that initializes RtcEngine with your Agora App ID, joins a channel with a temporary token (generated by a Cloud Function), and renders video using AgoraVideoView for both local and remote streams. Add control buttons for mute, camera, and end call.

Can I use Agora for group video calls in FlutterFlow, not just 1-on-1?

Yes. Agora channels support multiple participants. For group calls, maintain a List of remote UIDs in the widget's state — add UIDs in onUserJoined and remove them in onUserOffline. Render one AgoraVideoView per remote UID in a GridView. Agora supports up to 17 video publishers and hundreds of audience viewers per channel.

How much does Agora cost for a FlutterFlow video chat app?

Agora's free tier includes 10,000 minutes per month for voice and video combined. Beyond that, HD video costs approximately $3.99 per 1,000 minutes for the aggregate of all participants. A 30-minute 1-on-1 call uses 60 minutes (both sides combined). The free tier supports roughly 166 such calls per month.

Can I record video calls made through the FlutterFlow Agora integration?

Yes using Agora Cloud Recording. Call the Agora Cloud Recording API from a Cloud Function to start recording when a call begins. The recording is stored in an Agora-hosted or your own cloud storage bucket. This is a paid feature billed by recording minutes. You can also use Agora's composite recording to merge all participant videos into a single MP4 file.

How do I handle poor network conditions in the video call?

Agora automatically adapts video quality under poor network conditions (reduces resolution and frame rate). For your UI, listen to the onNetworkQuality callback and show a quality indicator. If quality drops below Agora's Poor level (3), show a toast warning the user. If the connection drops completely (onConnectionLost), show a Reconnecting overlay and attempt to rejoin the channel automatically.

What if I need a complete video calling feature with group rooms, recording, and moderator controls?

A production video platform with room management, recording, participant lists, hand-raising, screen sharing, and admin controls requires significant Custom Widget and Cloud Function development. RapidDev has built production Agora integrations in FlutterFlow and can deliver the complete video calling system.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.