Modern real-time communication demands meet cross-platform versatility — Flutter + ZEGO SDK delivers just that. Leveraging Flutter’s single-codebase multidevice deployment and ZEGO’s optimized AV engine, developers can rapidly build lightweight, low-latency video calling apps for mobile, desktop, and web.
1. Prerequisites & Environment Setup
Ensure the following minimum requirements for target platforms:
- Flutter ≥ 2.0
- iOS ≥ 12.0 (prefer real device)
- Android ≥ 4.4 (prefer real device, enable USB debugging)
- Windows ≥ 8
- macOS ≥ 10.14
- Web: Chrome ≥58, Firefox ≥56, Safari ≥11, Opera ≥45, Blink-based QQ/360 browser
- Linux: Debian ≥10, Ubuntu 20.04/22.04/24.04 LTS
Configure a supported IDE:
- Android Studio: Enable Flutter plugin and set SDK path
- VS Code: Install Flutter & Dart extensions
Run flutter doctor to install missing dependencies.
2. ZEGO Console Setup
- Sign up on ZEGO Cloud and log into the Console.
- Click Create Project → choose Real-Time Audio & Video.
- Copy the generated AppID (UUID format).
- Use the 24-hour temporary Token for development/testing (not production).
3. Integrate ZEGO Express SDK in Flutter
3.1 Create or Update Your Project
Generate a new project or open an existing one:
flutter create video_chat_app
3.2 Add SDK Dependency
Edit pubspec.yaml to include:
dependencies:
flutter:
sdk: flutter
zego_express_engine: ^3.10.3
Then run:
flutter pub get
3.3 Configure Native Permissions
iOS (lib/ios/Runner/Info.plist):
<key>NSCameraUsageDescription</key>
<string>Camera access is required for video calls</string>
<key>NSMicrophoneUsageDescription</key>
<string>Mic access is needed for real-time audio</string>
Android (android/app/src/main/AndroidManifest.xml):
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="true"/>
Dynamic permission request (Android):
// In your Activity class
private val CAMERA_MIC_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
private const val PERMISSION_REQUEST_CODE = 101
override fun requestPermissionsIfRequired() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val missing = CAMERA_MIC_PERMISSIONS.filterNot {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
if (missing.isNotEmpty()) {
requestPermissions(missing.toTypedArray(), PERMISSION_REQUEST_CODE)
}
}
}
4. Core Implementation Steps
4.1 Initialize Engine
import 'package:zego_express_engine/zego_express_engine.dart';
final appID = 1234567890; // decimal ID from console
final appSign =
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; // 64-char hex
ZegoEngineProfile profile = ZegoEngineProfile(
appID,
ZegoScenario.General,
appSign: appSign,
enablePlatformView: true,
);
ZegoExpressEngine.createEngineWithProfile(profile);
4.2 Join a Room
final user = ZegoUser(id: 'alice', name: 'Alice');
final roomConfig = ZegoRoomConfig.defaultConfig()
..isUserStatusNotify = true;
ZegoExpressEngine.instance.loginRoom('lobby', user, config: roomConfig);
Listen for Room Changes
ZegoExpressEngine.onRoomStateUpdate = (roomId, state, code, data) {
print('Room $roomId state: $state, code: $code');
};
ZegoExpressEngine.onRoomUserUpdate = (roomId, type, users) {
if (type == ZegoUpdateType.Add) {
print('Users joined: ${users.map((u) => u.id).join(", ")}');
}
};
ZegoExpressEngine.onRoomStreamUpdate = (roomId, type, streams) {
if (type == ZegoUpdateType.Add) {
streams.forEach((stream) => startRemotePlayback(stream.streamID));
}
};
4.3 Start Publishing Local Stream
ZegoExpressEngine.instance.startPublishingStream('local_stream_alice');
ZegoExpressEngine.onPublisherStateUpdate = (streamId, state, code, data) {
if (state == ZegoPublisherState.Error) {
print('Publish failed: $code on $streamId');
}
};
4.4 Play Remote Streams
Canvas setup (cross-platform):
Widget? remoteViewWidget;
int? remoteViewId;
void startRemotePlayback(String streamId) async {
remoteViewWidget = await ZegoExpressEngine.instance.createCanvasView((id) {
remoteViewId = id;
final canvas = ZegoCanvas.view(id);
ZegoExpressEngine.instance.startPlayingStream(streamId, canvas: canvas);
});
}
Destroy view after use:
if (remoteViewId != null) {
ZegoExpressEngine.instance.destroyCanvasView(remoteViewId!);
remoteViewId = null;
}
Stream playback listener
ZegoExpressEngine.onPlayerStateUpdate = (streamId, state, code, data) {
if (state == ZegoPlayerState.Playing) {
print('Playing stream $streamId');
}
};
4.5 Manage Lifecycle
- Stop publishing:
stopPublishingStream() - Stop preview:
stopPreview(); destroyCanvasView(previewId) - Stop playing:
stopPlayingStream(streamId) - Leave room:
logoutRoom(roomId) - Release engine:
ZegoExpressEngine.destroyEngine()
5. Testing & Debug
- Run on real device(s) or simulator/emulator.
- Use ZEGO’s Web test page: enter same
AppID,RoomID, andTokento interop with mobile/desktop clients. - Verify audio/video in both directions. Check console for error-level logs if issues arise.