Quick Start in Flutter using BLoC
VideoSDK enables you to embed video calling functionality into your Flutter applications within minutes.
In this quick start, we’ll walk through integrating group video calling using Flutter Video SDK with BLoC for state management. You’ll learn how to architect your application using BLoC to manage state cleanly and efficiently.
This guide will get you up and running with VideoSDK’s video & audio calling along with BLoC state management in minutes.
Prerequisites
Before proceeding, ensure that your development environment meets the following requirements:
- Video SDK Developer Account (Not having one, follow Video SDK Dashboard)
- Familiarity with Flutter and basic BLoC concepts.
- Flutter Video SDK
- Have Flutter 3.41 or later installed on your device.
One should have a VideoSDK account to generate token. Visit VideoSDK dashboard to generate token
Getting Started with the Code!
Follow the steps to create the environment necessary to add video calls into your app. Also you can find the code sample for quickstart here (this example uses an older SDK version).
Create a new Flutter project.
Create a new Flutter App using the below command.
$ flutter create flutter_bloc_quickstart
Install Required Dependencies
Navigate to your project directory and run the following commands to install the necessary dependencies:
$ flutter pub add videosdk:^5.0.0
Then pin permission_handler, which the SDK uses to ask for camera and microphone permission, to version 12. Version 13 brings in permission_handler_android 14, which builds only with Android Gradle Plugin 9, so an app created with Flutter 3.41 fails to build for Android.
$ flutter pub add permission_handler:^12.0.1
Run the following command to add the http library, which is used to make the network call that generates a roomId.
$ flutter pub add http
Run the following commands to add the BLoC packages used for state management.
$ flutter pub add flutter_bloc
$ flutter pub add equatable
Video SDK Compatibility
The Flutter SDK runs on Android and iOS. Web, macOS and Windows are not supported.
Structure of the project
Your project structure should look like this.
root
├── android
├── ios
├── lib
├── bloc
│ ├── room_cubit.dart
│ └── room_view_state.dart
├── api_call.dart
├── join_screen.dart
├── main.dart
├── room_controls.dart
├── room_screen.dart
└── participant_tile.dart
We are going to create flutter widgets (JoinScreen, RoomScreen, RoomControls, ParticipantTile, room_cubit, room_view_state).
App Structure
App widget will contain JoinScreen and RoomScreen widget. RoomScreen will have RoomControls and ParticipantTile widget.

Configure Project
For Android
- Update the
/android/app/src/main/AndroidManifest.xmlfor the permissions we will be using to implement the audio and video features.
<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"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
- Also you will need to set your build settings to Java 17, because recent versions of the Android Gradle Plugin and the SDK's Android dependencies require it. Just add this to your app-level
/android/app/build.gradle.kts.
android {
//...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
-
If necessary, in the same
build.gradle.ktsyou will need to increaseminSdkofdefaultConfigup to24(currently default Flutter generator set it to24). -
The SDK ships its native libraries for
arm64-v8aandarmeabi-v7a, so it does not run on x86 and x86_64 emulators. Test on an ARM device or on an emulator with anarm64-v8asystem image. To build your app forarm64-v8aandarmeabi-v7aonly, add an ABI filter todefaultConfigin the samebuild.gradle.kts.
android {
defaultConfig {
//...
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
}
}
- Then add this line to
/android/gradle.properties. Without it, Flutter adds its own ABIs to the filter and yours has no effect.
disable-abi-filtering=true
- For release builds, keep the SDK's classes. Flutter shrinks every Android release build with R8, which removes or renames the
videosdk.orgclasses that the SDK loads by name. Add these rules to/android/app/proguard-rules.pro, and create the file if it does not exist. Keep the-dontwarnline, because the keep rule alone fails the build.
-keep class videosdk.org.** { *; }
-dontwarn videosdk.org.**
If your project still uses the Groovy DSL (android/app/build.gradle), the same property assignments work there too. The older compileSdkVersion and minSdkVersion method syntax is deprecated as of Android Gradle Plugin 9 and will warn during the build.
For iOS
- Add the following entries which allow your app to access the camera and microphone to your
/ios/Runner/Info.plistfile :
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
- The SDK's iOS plugin ships as a Swift Package, so it needs Flutter 3.41 or later with Swift Package Manager turned on. Turn it on for your project by adding
config:under the existingflutter:section of your app'spubspec.yaml, or runflutter config --enable-swift-package-manageron every machine that builds your app.
flutter:
config:
enable-swift-package-manager: true
Step 1: Get started with api_call.dart
Before jumping to anything else, you will write a function to generate a unique room ID. You will require auth token, you can generate it using either by using videosdk-rtc-api-server-examples or generate it from the Video SDK Dashboard for development.
import 'dart:convert';
import 'package:http/http.dart' as http;
//Auth token we will use to generate a room and connect to it
String token = "<Generated-from-dashboard>";
// API call to create room
Future<String> createRoomId() 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'];
}
Step 2: Create BLoC Files
To manage room logic and state cleanly, we’ll use BLoC architecture. This includes two key files:
room_cubit.dart
Handles all room-related logic:
- Initializes the VideoSDK
Room, asks for camera and microphone permission, and joins it. - Listens to room events (
roomJoined,participantJoined, etc.). - Manages local/remote participant states and their video streams.
- Provides functions like
toggleMic,toggleCam,leaveRoom. - The room releases itself after it leaves the room.
This acts as the controller for our app's room behavior.
The SDK does not ask for camera and microphone permission on Android, so the cubit asks with VideoSDK.requestPermissions() before it joins. join(), leave() and the mic and camera actions return a Future that throws a VideoSDKError when the action fails, so the cubit awaits them inside try / catch. Stream events are delivered on the Room, and stream.participantId tells whose stream it is.
import 'dart:async' show unawaited;
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
part 'room_view_state.dart';
/// Cubit to manage video room state using VideoSDK
class RoomCubit extends Cubit<RoomViewState> {
// The room instance from VideoSDK
late Room _room;
/// Initializes with default state
RoomCubit()
: super(
RoomViewState(
isMicOff: false,
isVideoOff: false,
participants: {},
participantVideoStreams: {},
),
);
void initializeRoom(String token, String roomId, BuildContext context) {
// create room
_room = VideoSDK.createRoom(
roomId: roomId,
token: token,
participantName: "John Doe",
micEnabled: true,
camEnabled: true,
defaultCamera: FacingMode.FRONT, // camera used when the room starts
);
// Set room and add listeners
setRoom(_room, context);
// Join room
unawaited(_joinRoom());
}
Future<void> _joinRoom() async {
// On Android, the SDK does not ask for camera and microphone permission.
await VideoSDK.requestPermissions();
try {
await _room.join();
} on VideoSDKError catch (error) {
log("Could not join the room: ${error.code} ${error.name}");
}
}
/// Sets the room and registers room event listeners
void setRoom(Room room, BuildContext context) {
_room = room;
_setRoomEventListener(context);
}
/// Handles room and participant lifecycle events
void _setRoomEventListener(BuildContext context) {
// Triggered when the local participant successfully joins the room.
_room.on(Events.roomJoined, () {
final updated = Map<String, Participant>.from(state.participants);
updated[_room.localParticipant.id] = _room.localParticipant;
_initializeParticipantStreams(_room.localParticipant);
emit(state.copyWith(participants: updated));
});
// Triggered when a remote participant joins the room.
_room.on(Events.participantJoined, (Participant participant) {
final updated = Map<String, Participant>.from(state.participants);
updated[participant.id] = participant;
_initializeParticipantStreams(participant);
emit(state.copyWith(participants: updated));
});
// Triggered when a remote participant left the room.
_room.on(Events.participantLeft, (Participant participant, LeaveReason reason) {
final updated = Map<String, Participant>.from(state.participants);
final updatedStreams = Map<String, RTCStream?>.from(
state.participantVideoStreams,
);
updated.remove(participant.id);
updatedStreams.remove(participant.id);
emit(
state.copyWith(
participants: updated,
participantVideoStreams: updatedStreams,
),
);
});
// Triggered when a participant's video is turned on.
// stream.participantId is the id of the participant the stream belongs to.
_room.on(Events.streamEnabled, (RTCStream stream) {
if (stream.kind == MediaType.VIDEO) {
final newStreams = Map<String, RTCStream?>.from(
state.participantVideoStreams,
);
newStreams[stream.participantId] = stream;
emit(state.copyWith(participantVideoStreams: newStreams));
}
});
// Triggered when a participant's video is turned off.
_room.on(Events.streamDisabled, (RTCStream stream) {
if (stream.kind == MediaType.VIDEO) {
final newStreams = Map<String, RTCStream?>.from(
state.participantVideoStreams,
);
newStreams[stream.participantId] = null;
emit(state.copyWith(participantVideoStreams: newStreams));
}
});
// Triggered when the local participant successfully leave the room.
_room.on(Events.roomLeft, () {
emit(state.copyWith(participants: {}, participantVideoStreams: {}));
Navigator.popUntil(context, ModalRoute.withName('/'));
});
}
/// Picks up the video stream a participant already has
void _initializeParticipantStreams(Participant participant) {
final updatedStreams = Map<String, RTCStream?>.from(
state.participantVideoStreams,
);
// Initialize video stream
participant.streams.forEach((key, RTCStream stream) {
if (stream.kind == MediaType.VIDEO) {
updatedStreams[participant.id] = stream;
}
});
emit(state.copyWith(participantVideoStreams: updatedStreams));
}
/// Toggles the local participant's mic
Future<void> toggleMic() async {
try {
if (state.isMicOff) {
await _room.unmuteMic();
} else {
await _room.muteMic();
}
if (!isClosed) emit(state.copyWith(isMicOff: !state.isMicOff));
} on VideoSDKError catch (error) {
log("Could not toggle the mic: ${error.code} ${error.name}");
}
}
/// Toggles the local participant's camera
Future<void> toggleCam() async {
try {
if (state.isVideoOff) {
await _room.enableCam();
} else {
await _room.disableCam();
}
if (!isClosed) emit(state.copyWith(isVideoOff: !state.isVideoOff));
} on VideoSDKError catch (error) {
log("Could not toggle the camera: ${error.code} ${error.name}");
}
}
/// Leaves the current room
Future<void> leaveRoom() async {
try {
await _room.leave();
} on VideoSDKError catch (error) {
// 3054: the room has already left.
if (error.code != 3054) {
log("Could not leave the room: ${error.code} ${error.name}");
}
}
}
/// Room accessor for UI or other logic
Room get room => _room;
}
room_view_state.dart
Defines the immutable state used by RoomCubit:
isMicOff: Whether the mic is off.isVideoOff: Whether the camera is off.participants: A map of all participants in the room.participantVideoStreams: A map of each participant's active video stream.
part of 'room_cubit.dart';
/// Represents the current state of the room
class RoomViewState extends Equatable {
final bool isMicOff;
final bool isVideoOff;
final Map<String, Participant> participants;
final Map<String, RTCStream?> participantVideoStreams;
/// Creates a new immutable RoomViewState instance
const RoomViewState({
required this.isMicOff,
required this.isVideoOff,
required this.participants,
required this.participantVideoStreams,
});
/// Creates a copy of the current state with updated values
RoomViewState copyWith({
bool? isMicOff,
bool? isVideoOff,
Map<String, Participant>? participants,
Map<String, RTCStream?>? participantVideoStreams,
}) {
return RoomViewState(
isMicOff: isMicOff ?? this.isMicOff,
isVideoOff: isVideoOff ?? this.isVideoOff,
participants: participants ?? this.participants,
participantVideoStreams: participantVideoStreams ?? this.participantVideoStreams,
);
}
/// Used by Equatable to compare state instances efficiently
@override
List<Object?> get props => [
isMicOff,
isVideoOff,
participants,
participantVideoStreams,
];
}
Step 3 : Creating the JoinScreen
Let's create join_screen.dart file in lib directory and create JoinScreen StatelessWidget.
The JoinScreen will consist of:
- Create Room Button - This button will create a new room for you.
- Room ID TextField - This text field will contain the room ID, you want to join.
- Join Room Button - This button will join the room, which you have provided.
Both buttons navigate to the RoomScreen while injecting the RoomCubit using BlocProvider
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'api_call.dart';
import 'room_screen.dart';
import 'bloc/room_cubit.dart';
class JoinScreen extends StatelessWidget {
final _roomIdController = TextEditingController();
JoinScreen({super.key});
void onCreateButtonPressed(BuildContext context) async {
// call api to create room and then navigate to RoomScreen with roomId,token
await createRoomId().then((roomId) {
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider<RoomCubit>(
create: (_) => RoomCubit(),
child: RoomScreen(roomId: roomId, token: token),
),
),
);
});
}
void onJoinButtonPressed(BuildContext context) {
String roomId = _roomIdController.text;
var re = RegExp("\\w{4}\\-\\w{4}\\-\\w{4}");
// check room id is not null or invaild
// if room id is vaild then navigate to RoomScreen with roomId,token
if (roomId.isNotEmpty && re.hasMatch(roomId)) {
_roomIdController.clear();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider<RoomCubit>(
create: (_) => RoomCubit(),
child: RoomScreen(roomId: roomId, token: token),
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Please enter valid room id")),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('VideoSDK QuickStart')),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => onCreateButtonPressed(context),
child: const Text('Create Room'),
),
Container(
margin: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
child: TextField(
decoration: const InputDecoration(
hintText: 'Room Id',
border: OutlineInputBorder(),
),
controller: _roomIdController,
),
),
ElevatedButton(
onPressed: () => onJoinButtonPressed(context),
child: const Text('Join Room'),
),
],
),
),
);
}
}
- Update the home screen of the app in the
main.dart.
import 'package:flutter/material.dart';
import 'join_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'VideoSDK QuickStart',
theme: ThemeData(primarySwatch: Colors.blue),
home: JoinScreen(),
);
}
}
Step 4 : Creating the RoomControls
Let's create room_controls.dart file and create RoomControls StatelessWidget.
The RoomControls will consist of:
- Leave Button - This button will leave the room.
- Toggle Mic Button - This button will unmute or mute mic.
- Toggle Camera Button - This button will enable or disable camera.
RoomControls will accept 3 functions in constructor
- onLeaveButtonPressed - invoked when Leave button pressed
- onToggleMicButtonPressed - invoked when Toggle Mic button pressed
- onToggleCameraButtonPressed - invoked when Toggle Camera button pressed
import 'package:flutter/material.dart';
class RoomControls extends StatelessWidget {
final void Function() onToggleMicButtonPressed;
final void Function() onToggleCameraButtonPressed;
final void Function() onLeaveButtonPressed;
final bool isMicOff;
final bool isCamOff;
const RoomControls({
super.key,
required this.onToggleMicButtonPressed,
required this.onToggleCameraButtonPressed,
required this.onLeaveButtonPressed,
required this.isMicOff,
required this.isCamOff,
});
@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'),
),
ElevatedButton(
onPressed: onToggleCameraButtonPressed,
child: const Text('Toggle WebCam'),
),
],
);
}
}
Step 5 : Creating ParticipantTile
Let's create participant_tile.dart file and create ParticipantTile StatelessWidget.
The ParticipantTile will consist of:
- RTCVideoView - This will show participant's video stream.
ParticipantTile will accept Participant in constructor
- participant - participant of the room.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:videosdk/videosdk.dart';
import 'bloc/room_cubit.dart';
class ParticipantTile extends StatelessWidget {
final Participant participant;
const ParticipantTile({super.key, required this.participant});
@override
Widget build(BuildContext context) {
return BlocBuilder<RoomCubit, RoomViewState>(
builder: (context, state) {
final videoStream = state.participantVideoStreams[participant.id];
return Padding(
padding: const EdgeInsets.all(8.0),
child: videoStream != null
? RTCVideoView(
videoStream.renderer as RTCVideoRenderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)
: Container(
color: Colors.grey.shade800,
child: const Center(child: Icon(Icons.person, size: 100)),
),
);
},
);
}
}
Step 6 : Creating the RoomScreen
Let's create room_screen.dart file and create RoomScreen StatefulWidget.
RoomScreen will accept room ID and token in constructor
- room ID - room ID, you want to join
- token - VideoSdk Auth token
BLoC Usage:
- Uses
RoomCubitto initialize and manage the room state. - Calls
initializeRoomonce, ininitState, to join the room usingtoken,roomId, andcontext. - Uses
BlocBuilderto rebuild UI based on changes inRoomViewState. - Handles room controls (toggle mic/cam, leave) by invoking corresponding methods from
RoomCubit. - Leaves the room when the back button closes the screen.
BlocProviderthen closes the cubit, which releases the room.
import 'dart:async' show unawaited;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'participant_tile.dart';
import 'room_controls.dart';
import 'bloc/room_cubit.dart';
class RoomScreen extends StatefulWidget {
final String roomId;
final String token;
const RoomScreen({
super.key,
required this.roomId,
required this.token,
});
@override
State<RoomScreen> createState() => _RoomScreenState();
}
class _RoomScreenState extends State<RoomScreen> {
@override
void initState() {
super.initState();
// Initialize room once, when the screen opens
context
.read<RoomCubit>()
.initializeRoom(widget.token, widget.roomId, context);
}
@override
Widget build(BuildContext context) {
return PopScope(
// leave the room when the back button closes the screen
onPopInvokedWithResult: (didPop, result) {
if (didPop) unawaited(context.read<RoomCubit>().leaveRoom());
},
child: Scaffold(
appBar: AppBar(title: const Text('VideoSDK QuickStart')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(widget.roomId),
//render all participant
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: BlocBuilder<RoomCubit, RoomViewState>(
builder: (context, state) {
final participants = state.participants.values.toList();
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
mainAxisExtent: 300,
),
itemBuilder: (context, index) {
return ParticipantTile(
key: Key(participants[index].id),
participant: participants[index],
);
},
itemCount: participants.length,
);
},
),
),
),
BlocBuilder<RoomCubit, RoomViewState>(
builder: (context, state) {
return RoomControls(
onToggleMicButtonPressed: () {
unawaited(context.read<RoomCubit>().toggleMic());
},
onToggleCameraButtonPressed: () {
unawaited(context.read<RoomCubit>().toggleCam());
},
onLeaveButtonPressed: () {
unawaited(context.read<RoomCubit>().leaveRoom());
},
isMicOff: state.isMicOff,
isCamOff: state.isVideoOff,
);
},
),
],
),
),
),
);
}
}
Output
Run and Test
The app is all set to test. Make sure to update the token in api_call.dart
Your app should look like this after the implementation.
Run the app on an ARM Android device or an arm64-v8a emulator, and on an iPhone, an iPad or the iOS Simulator on a Mac with Apple silicon. x86 and x86_64 Android emulators and the iOS Simulator on an Intel Mac are not supported. See Known Issues.
You can checkout the complete flutter bloc example here (this example uses an older SDK version).
Got a Question? Ask us on discord

