DEV Community

Hazem Hamdy
Hazem Hamdy

Posted on

How to Build a Real-Time Video Calling App in Flutter Using ZEGOCLOUD (No WebRTC Needed)

🔰 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:

Screenshot showing ZEGOCLOUD dependencies added in pubspec.yaml file

Then run:

flutter pub get
# install dependencies
Enter fullscreen mode Exit fullscreen mode

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:

Flutter constant.dart file containing ZEGOCLOUD AppID and AppSign values

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

⚙️ Step — Configure android/app/build.gradle

Open this file and make sure your Gradle setup is correct:
Android build.gradle configuration showing compileSdk 36 and Java version setup

🔧 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
Enter fullscreen mode Exit fullscreen mode

✏️ Add or Update the Following Line

distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
Enter fullscreen mode Exit fullscreen mode

📡 Required Android Permissions

Add the following permissions inside your AndroidManifest.xml:

🛡 Proguard Setup (Release mode only)

Inside build.gradle add:


android/app/
Enter fullscreen mode Exit fullscreen mode

Then create the file in app progurad-rules.pro:

🔊 Add Audio Files in raw Folder

Create the folder:

android/app/src/main/res/raw
Enter fullscreen mode Exit fullscreen mode

Place your audio files inside it such as:


you can follow the link in GitHub to download:

Link:
https://github.com/ZEGOCLOUD/zego_uikit_prebuilt_call_example_flutter/tree/master/call_with_offline_invitation/android/app/src/main/res/raw

🟣 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
Enter fullscreen mode Exit fullscreen mode

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'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

Screenshot of the Calling Page UI where the user enters a User ID and User Name before starting a call

📞 Incoming Call Preview

Screenshot showing an incoming voice call alert UI with accept and decline buttons powered by ZEGOCLOUD

Description:

When a call invitation is received, ZEGOCLOUD displays a native call UI with accept/decline buttons—no UI coding required

🎥 Video Call Interface

Live video call screen showing two connected users with camera preview, mic controls, and call actions using ZEGOCLOUD UI

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)