🔰 How to Build a Video Calling App in Flutter Using ZEGOCLOUD — Step by Step
📌 Introduction
Video calling has become an essential part of modern applications—whether for online education, remote meetings, or technical support. Building a real-time communication system from scratch using WebRTC can be complicated and time-consuming. Luckily, services like ZEGOCLOUD simplify the entire process by providing a ready-to-use SDK that allows developers to integrate high-quality video calls into their apps with minimal effort.
In this guide, we’ll build a simple Flutter application that supports real-time video calls using the ZEGOCLOUD Prebuilt Call SDK. The steps are beginner-friendly and designed to help you get your first video calling app running quickly.
🎯 What You’ll Learn
By the end of this tutorial, you will be able to:
- Understand what ZEGOCLOUD is and how it works
- Integrate ZEGOCLOUD’s Video Call SDK into a Flutter project
- Create a UI that allows users to join and start video calls
- Run the app on two devices and test a real video call interaction
🔍 What Is ZEGOCLOUD?
ZEGOCLOUD is a cloud communication platform that provides ready-made solutions for video calls, voice calls, live streaming, and real-time chat. Instead of building a signaling layer and media engine yourself, you simply integrate their SDK and use your AppID and AppSign—and you’re good to go.
✋ Before We Start – Prerequisites
To follow along, make sure you have:
✔ Flutter installed on your machine
✔ A new Flutter project ready
✔ A ZEGOCLOUD account to generate:
- AppID
- AppSign
You can get them from the official documentation:
🔗 https://www.zegocloud.com/docs/uikit/callkit-flutter/quick-start
We’ll also use Firebase for storing basic user data and handling logout functionality:
🔗 https://console.firebase.google.com/u/0/
There’s also one more important step—editing your android/app/build.gradle. You must configure it correctly for the SDK to work. This is explained in the official docs, so make sure you follow it carefully.
🟢 Step 1 — Install the ZEGOCLOUD SDK
Open your pubspec.yaml file and add the ZEGOCLOUD dependency:
Then run:
flutter pub get
# install dependencies
This installs everything needed for video calling in your project.
🔑 Step — Add Your ZEGOCLOUD Keys
Create a new file named constant.dart and insert your AppID and AppSign inside it:
🟣 main file Initialize
import 'package:zego_uikit/zego_uikit.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart';
import 'package:zego_uikit_signaling_plugin/zego_uikit_signaling_plugin.dart';
import 'package:zegotest/calling_page.dart';
import 'package:zegotest/firebase_options.dart';
import 'package:zegotest/login_page.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late SharedPreferences sharedPreferences;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
sharedPreferences = await SharedPreferences.getInstance();
ZegoUIKitPrebuiltCallInvitationService().setNavigatorKey(navigatorKey);
try {
await ZegoUIKit().initLog();
ZegoUIKitPrebuiltCallInvitationService().useSystemCallingUI([
ZegoUIKitSignalingPlugin(),
]);
debugPrint('✅ ZegoUIKit initialized successfully');
} catch (e) {
debugPrint('❌ Error initializing ZegoUIKit: $e');
}
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: navigatorKey,
title: 'Zego Call Test',
//theme: ThemeData.dark(),
home: sharedPreferences.getString('id') != null
? CallingPage()
: const LoginPage(),
);
}
}
Make sure you never expose your AppSign in a public repository.
🟣 Step — Create the Call Service
Before users can start or receive calls, we need a service that initializes the ZEGOCLOUD call system once the user logs in, and cleans everything up when they log out.
This ensures the SDK is ready to handle invitations, notifications, and real-time call updates.
Create a new file named:
callservices.dart
Then paste the following code:
import 'package:flutter/material.dart';
import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart';
import 'package:zego_uikit_signaling_plugin/zego_uikit_signaling_plugin.dart';
import 'package:zegotest/constant.dart';
import 'package:zego_uikit/zego_uikit.dart';
class Callservices {
/// on App's user login
Future<void> onUserLogin(String userID, String userName) async {
/// Initialize ZegoUIKitPrebuiltCallInvitationService
/// We recommend calling this method as soon as the user logs in.
await ZegoUIKitPrebuiltCallInvitationService().init(
appID: Constant.appId,
appSign: Constant.appSign,
userID: userID,
userName: userName,
plugins: [ZegoUIKitSignalingPlugin()],
/// Optional custom configuration
requireConfig: (ZegoCallInvitationData data) {
return ZegoUIKitPrebuiltCallConfig.groupVideoCall()
..avatarBuilder = (
BuildContext context,
Size size,
ZegoUIKitUser? user,
Map extraInfo,
) {
return user?.name.isNotEmpty ?? false
? ClipOval(
child: Container(
width: size.width,
height: size.height,
color: Colors.blue,
child: Center(
child: Text(
user!.name.substring(0, 1).toUpperCase(),
style: const TextStyle(color: Colors.white),
),
),
),
)
: ClipOval(
child: Container(
color: Colors.blue,
child: Center(
child: Text(
user?.name.isNotEmpty ?? false
? user!.name.substring(0, 1).toUpperCase()
: 'U',
style: TextStyle(
color: Colors.white,
fontSize: size.width / 2,
fontWeight: FontWeight.bold,
),
),
),
),
);
};
},
);
}
/// on App's user logout
void onUserLogout() {
/// Unregister service and free resources
ZegoUIKitPrebuiltCallInvitationService().uninit();
}
}
⚙️ Step — Configure android/app/build.gradle
Open this file and make sure your Gradle setup is correct:

🔧 Update Gradle Wrapper Configuration
To ensure compatibility with ZEGOCLOUD and the latest Android tooling, you must update your Gradle wrapper version. This step is required for proper SDK integration.
📂 File Path
android/gradle/wrapper/gradle-wrapper.properties
✏️ Add or Update the Following Line
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
📡 Required Android Permissions
Add the following permissions inside your AndroidManifest.xml:
🛡 Proguard Setup (Release mode only)
Inside build.gradle add:
android/app/
Then create the file in app progurad-rules.pro:
🔊 Add Audio Files in raw Folder
Create the folder:
android/app/src/main/res/raw
Place your audio files inside it such as:
you can follow the link in GitHub to download:
🟣 Step 3 — Create the Video Call Screen
The goal here is to provide a screen where a user can join or start a call. ZEGOCLOUD gives us a prebuilt UI, so we don’t need to build everything from scratch.
Create a new file:
🟡 Step — Add the Login Screen
Before users can join or start a call, we need a simple login screen that collects a User ID and Username. These two values are required by ZEGOCLOUD to identify each participant in the call.
Create a new file named:
login_page.dart
and paste the following code:
import 'package:flutter/material.dart';
import 'package:zegotest/calling_page.dart';
import 'package:zegotest/callservices.dart';
import 'package:zegotest/main.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController username = TextEditingController();
final TextEditingController id = TextEditingController();
final GlobalKey<FormState> globleKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[200],
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: globleKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.lock, size: 70, color: Colors.blue),
const SizedBox(height: 20),
const Text(
"Welcome Back!",
style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
),
const SizedBox(height: 30),
// ID Field
TextFormField(
controller: id,
decoration: InputDecoration(
labelText: "ID",
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
validator: (value) =>
value == null || value.isEmpty ? "Enter ID" : null,
),
const SizedBox(height: 15),
// Username Field
TextFormField(
controller: username,
decoration: InputDecoration(
labelText: "User Name",
prefixIcon: const Icon(Icons.badge),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
validator: (value) => value == null || value.isEmpty
? "Enter User Name"
: null,
),
const SizedBox(height: 25),
// Login Button
ElevatedButton(
onPressed: () {
if (globleKey.currentState!.validate()) {
sharedPreferences.setString('id', id.text.trim());
sharedPreferences.setString(
'userName',
username.text.trim(),
);
Callservices callservices = Callservices();
callservices.onUserLogin(
id.text.trim(),
username.text.trim(),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Logged in as ${username.text} (ID: ${id.text})",
),
backgroundColor: Colors.green,
),
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CallingPage(),
),
);
}
},
child: const Text('Login'),
),
],
),
),
),
),
);
}
}
call page
import 'package:flutter/material.dart';
import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart';
import 'package:zegotest/callservices.dart';
import 'package:zegotest/login_page.dart';
import 'package:zegotest/main.dart';
class CallingPage extends StatefulWidget {
const CallingPage({super.key});
@override
State<CallingPage> createState() => _CallingPageState();
}
class _CallingPageState extends State<CallingPage> {
final TextEditingController userId = TextEditingController();
final TextEditingController userName = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Calling Page")),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextFormField(
controller: userId,
decoration: InputDecoration(
labelText: "User ID",
prefixIcon: const Icon(Icons.badge),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (_) {
setState(() {}); // ✅ عشان الinvitees تتحدث أول بأول
},
),
SizedBox(height: 10),
TextFormField(
controller: userName,
decoration: InputDecoration(
labelText: "User Name",
prefixIcon: const Icon(Icons.badge),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
SizedBox(height: 20),
CustomButtonCall(userId: userId, userName: userName),
CustomButtonLogout(),
],
),
),
);
}
}
class CustomButtonLogout extends StatelessWidget {
const CustomButtonLogout({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
Callservices callservices = Callservices();
callservices.onUserLogout();
sharedPreferences.clear();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => LoginPage()),
(route) => false,
);
},
child: Text('Log Out'),
);
}
}
class CustomButtonCall extends StatelessWidget {
const CustomButtonCall({
super.key,
required this.userId,
required this.userName,
});
final TextEditingController userId;
final TextEditingController userName;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
ZegoUIKitPrebuiltCallInvitationService().send(
invitees: [ZegoCallUser(userId.text, userName.text)],
isVideoCall: false,
customData: "Hello World",
);
},
child: Text('Call Video'),
);
}
}
Once added, this screen will handle joining, leaving, and displaying the video call interface.
🟢 Step 4 — Start the Call
Now let’s connect everything with a simple button on the home screen. The user enters a Call ID, taps a button, and joins the call.
Once two users enter the same Call ID from different devices, they’ll instantly be connected.
Once the user logs in, they land here and can start a call.
📞 Incoming Call Preview
Description:
When a call invitation is received, ZEGOCLOUD displays a native call UI with accept/decline buttons—no UI coding required
🎥 Video Call Interface
Description:
Here’s the live video call running between two devices using ZEGOCLOUD’s prebuilt UI. Camera switching, mic control, and call end functionality are all ready out of the box.
🔁 What’s Next?
You can now:
- Test calls between two devices
- Add user authentication
- Customize the UI
- Explore more features from the ZEGOCLOUD ecosystem
And this is just the beginning—there’s a lot more we can build on top of this functionality.
🎉 Conclusion
You've just built a fully functional real-time video calling app in Flutter — without writing a WebRTC engine, without media servers, and without struggling with signaling logic.
Thanks to ZEGOCLOUD, all the heavy lifting is handled for you.
🚀 Now it's your turn…
Try inviting a friend, run the app on two devices, and enjoy your first live call!
If you want to go further, you can:
- Customize the call UI
- Add push notifications
- Support group video calls
- Integrate chat inside the call screen
The possibilities are endless.








Top comments (0)