Quick Start in Flutter using RiverPod
VideoSDK enables you to embed video calling functionality into your Flutter applications within minutes.
In this quick start guide, we’ll walk through integrating group video calling using Flutter Video SDK with Riverpod for state management. You’ll learn how to architect your application using Riverpod to manage state in a clean, modular, and efficient way.
This guide will get you up and running with VideoSDK’s video & audio calling using Riverpod state management in just a few 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 Riverpod concepts.
- Flutter Video SDK
- Have Flutter 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.
Create a new Flutter project.
Create a new Flutter App using the below command.
$ flutter create flutter_riverpod_quickstart
Install Required Dependencies
Navigate to your project directory and run the following commands to install the necessary dependencies:
$ flutter pub add videosdk
//run this command to add http library to perform network call to generate roomId
$ flutter pub add http
// Add Riverpod (Flutter's state management library)
$ flutter pub add riverpod
// Add Riverpod annotations for code generation with @riverpod and @Riverpod
$ flutter pub add riverpod_annotation
// Add build_runner to generate code for Riverpod (and other code-gen tools)
$ flutter pub add --dev build_runner
$ flutter pub add --dev riverpod_generator
Video SDK Compatibility
Android and iOS | Web (Beta) | Desktop (Beta) | Safari |
---|---|---|---|
Structure of the project
Your project structure should look like this.
root
├── android
├── ios
├── lib
├── riverpod
│ ├── meeting_controller.dart
│ └── meeting_state.dart
├── api_call.dart
├── join_screen.dart
├── main.dart
├── meeting_controls.dart
├── meeting_screen.dart
└── participant_tile.dart
We are going to create flutter widgets (JoinScreen, MeetingScreen, MeetingControls, ParticipantTile, meeting_controller, meeting_state).
App Structure
App widget will contain JoinScreen
and MeetingScreen
widget. MeetingScreen
will have MeetingControls
and ParticipantTile
widget.
Configure Project
For Android
- Update the
/android/app/src/main/AndroidManifest.xml
for 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" />
For iOS
- Add the following entries which allow your app to access the camera and microphone to your
/ios/Runner/Info.plist
file :
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
- Uncomment the following line to define a global platform for your project in
/ios/Podfile
:
# platform :ios, '12.0'
For MacOS
- Add the following entries to your
/macos/Runner/Info.plist
file which allow your app to access the camera and microphone.
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
- Add the following entries to your
/macos/Runner/DebugProfile.entitlements
file which allow your app to access the camera, microphone and open outgoing network connections.
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
- Add the following entries to your
/macos/Runner/Release.entitlements
file which allow your app to access the camera, microphone and open outgoing network connections.
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
Step 1: Get started with api_call.dart
Before jumping to anything else, you will write a function to generate a unique meetingId. 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 meeting and connect to it
String token = "<Generated-from-dashboard>";
// API call to create meeting
Future<String> createMeeting() 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 Riverpod Files
To manage meeting logic and state cleanly, we’ll use Riverpod architecture. This includes two key files:
meeting_controller.dart
This file defines the Riverpod controller that handles all meeting logic using @riverpod
and state generation. It is responsible for:
- Initializes the VideoSDK
Room
. - Listens to meeting events (
roomJoined
,participantJoined
, etc.). - Manages local/remote participant states and their video streams.
- Provides functions like
toggleMic
,toggleCam
,leaveMeeting
.
This acts as the controller for our app's meeting behavior.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:videosdk/videosdk.dart';
import 'meeting_state.dart';
// Generated file for Riverpod using build_runner
part 'meeting_controller.g.dart';
// Define a Riverpod provider using @riverpod annotation and code generation
@riverpod
class MeetingController extends _$MeetingController {
// Room instance from VideoSDK
late Room _room;
@override
MeetingState build((String token, String meetingId, BuildContext context) args) {
final token = args.$1;
final meetingId = args.$2;
final context = args.$3;
// create room
_room = VideoSDK.createRoom(
roomId: meetingId,
token: token,
displayName: "John Doe",
micEnabled: true,
camEnabled: true,
defaultCameraIndex: kIsWeb ? 0 : 1,
);
// Set room and add listeners
_setRoomListeners(context);
// Join room
_room.join();
return const MeetingState();
}
// Handles room and participant lifecycle events
void _setRoomListeners(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;
_initParticipantStreams(_room.localParticipant);
state = 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;
_initParticipantStreams(participant);
state = state.copyWith(participants: updated);
});
// Triggered when a remote participant left the room.
_room.on(Events.participantLeft, (String participantId) {
final updated = Map<String, Participant>.from(state.participants);
final updatedStreams = Map<String, Stream?>.from(
state.participantVideoStreams,
);
updated.remove(participantId);
updatedStreams.remove(participantId);
state = state.copyWith(
participants: updated,
participantVideoStreams: updatedStreams,
);
});
// Triggered when the local participant successfully leave the room.
_room.on(Events.roomLeft, () {
state = state.copyWith(participants: {}, participantVideoStreams: {});
Navigator.popUntil(context, ModalRoute.withName('/'));
});
}
/// Initializes and listens to participant video stream changes
void _initParticipantStreams(Participant participant) {
final updatedStreams = Map<String, Stream?>.from(state.participantVideoStreams);
// Initialize video stream
participant.streams.forEach((key, Stream stream) {
if (stream.kind == 'video') {
updatedStreams[participant.id] = stream;
}
});
// Video stream enabled
participant.on(Events.streamEnabled, (Stream stream) {
if (stream.kind == 'video') {
final newStreams = Map<String, Stream?>.from(state.participantVideoStreams);
newStreams[participant.id] = stream;
state = state.copyWith(participantVideoStreams: newStreams);
}
});
// Video stream disabled
participant.on(Events.streamDisabled, (Stream stream) {
if (stream.kind == 'video') {
final newStreams = Map<String, Stream?>.from(state.participantVideoStreams);
newStreams[participant.id] = null;
state = state.copyWith(participantVideoStreams: newStreams);
}
});
state = state.copyWith(participantVideoStreams: updatedStreams);
}
/// Toggles the local participant's mic
void toggleMic() {
if (state.isMicOff) {
_room.unmuteMic();
} else {
_room.muteMic();
}
state = state.copyWith(isMicOff: !state.isMicOff);
}
/// Toggles the local participant's camera
void toggleCam() {
if (state.isVideoOff) {
_room.enableCam();
} else {
_room.disableCam();
}
state = state.copyWith(isVideoOff: !state.isVideoOff);
}
/// Leaves the current meeting
void leaveMeeting() {
_room.leave();
}
/// Room accessor for UI or other logic
Room get room => _room;
}
meeting_state.dart
Defines the immutable state used by MeetingCubit
:
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.
import 'package:videosdk/videosdk.dart';
class MeetingState {
final bool isMicOff;
final bool isVideoOff;
final Map<String, Participant> participants;
final Map<String, Stream?> participantVideoStreams;
// Constructor with default values
const MeetingState({
this.isMicOff = false,
this.isVideoOff = false,
this.participants = const {},
this.participantVideoStreams = const {},
});
// Creates a copy of the state with optional overrides
MeetingState copyWith({
bool? isMicOff,
bool? isVideoOff,
Map<String, Participant>? participants,
Map<String, Stream?>? participantVideoStreams,
}) {
return MeetingState(
isMicOff: isMicOff ?? this.isMicOff,
isVideoOff: isVideoOff ?? this.isVideoOff,
participants: participants ?? this.participants,
participantVideoStreams: participantVideoStreams ?? this.participantVideoStreams,
);
}
}
Generate .g.dart
File
Since we're using @riverpod
, we need to generate the .g.dart
file for meeting_controller.dart
.
To genrate .g.dart
file, run the following command in your terminal:
dart run build_runner build
If you’re working on meeting_controller.dart
frequently, you can run:
dart run build_runner watch
Step 3 : Creating the JoinScreen
Let's create join_screen.dart
file in lib
directory and create JoinScreen ConsumerWidget
.
The JoinScreen will consist of:
- Create Meeting Button - This button will create a new meeting for you.
- Meeting ID TextField - This text field will contain the meeting ID, you want to join.
- Join Meeting Button - This button will join the meeting, which you have provided.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'api_call.dart';
import 'meeting_screen.dart';
class JoinScreen extends ConsumerWidget {
final TextEditingController _meetingIdController = TextEditingController();
JoinScreen({super.key});
void onCreateButtonPressed(BuildContext context, WidgetRef ref) async {
// call api to create meeting and then navigate to MeetingScreen with meetingId,token
final meetingId = await createMeeting();
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MeetingScreen(meetingId: meetingId, token: token),
),
);
}
}
void onJoinButtonPressed(BuildContext context, WidgetRef ref) {
final meetingId = _meetingIdController.text.trim();
final re = RegExp(r'\w{4}-\w{4}-\w{4}');
// check meeting id is not null or invaild
// if meeting id is vaild then navigate to MeetingScreen with meetingId,token
if (meetingId.isNotEmpty && re.hasMatch(meetingId)) {
_meetingIdController.clear();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MeetingScreen(meetingId: meetingId, token: token),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Please enter a valid meeting ID")),
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
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, ref),
child: const Text('Create Meeting'),
),
Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
controller: _meetingIdController,
decoration: const InputDecoration(
hintText: 'Meeting ID',
border: OutlineInputBorder(),
),
),
),
ElevatedButton(
onPressed: () => onJoinButtonPressed(context, ref),
child: const Text('Join Meeting'),
),
],
),
),
);
}
}
- Update the home screen of the app in the
main.dart
.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'join_screen.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'VideoSDK QuickStart',
theme: ThemeData(primarySwatch: Colors.blue),
home: JoinScreen(),
);
}
}
Step 4 : Creating the MeetingControls
Let's create meeting_controls.dart
file and create MeetingControls StatelessWidget
.
The MeetingControls will consist of:
- Leave Button - This button will leave the meeting.
- Toggle Mic Button - This button will unmute or mute mic.
- Toggle Camera Button - This button will enable or disable camera.
MeetingControls 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 MeetingControls extends StatelessWidget {
final void Function() onToggleMicButtonPressed;
final void Function() onToggleCameraButtonPressed;
final void Function() onLeaveButtonPressed;
final bool isMicOff;
final bool isCamOff;
const MeetingControls({
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: Text('Toggle Mic'),
),
ElevatedButton(
onPressed: onToggleCameraButtonPressed,
child: Text('Toggle Cam'),
),
],
);
}
}
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 meeting.
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class ParticipantTile extends StatelessWidget {
final Participant participant;
final Stream? videoStream;
const ParticipantTile({
super.key,
required this.participant,
required this.videoStream,
});
@override
Widget build(BuildContext context) {
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 MeetingScreen
Let's create a meeting_screen.dart
file and define the MeetingScreen
widget as a ConsumerWidget
.
MeetingScreen will accept meetingId and token in constructor
- meetingId - meetingId, you want to join
- token - VideoSdk Auth token
Riverpod Usage:
- Uses
meetingControllerProvider
with a family parameter to initialize the meeting usingtoken
,meetingId
, andcontext
. - Watches the provider for
MeetingState
to update UI reactively. - Reads the provider notifier to invoke meeting control actions.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'riverpod/meeting_controller.dart';
import 'meeting_controls.dart';
import 'participant_tile.dart';
class MeetingScreen extends ConsumerWidget {
final String meetingId;
final String token;
const MeetingScreen({
super.key,
required this.meetingId,
required this.token,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch meeting state from Riverpod provider to update UI
final meetingState = ref.watch(
meetingControllerProvider((token, meetingId, context)),
);
// Read notifier to perform actions like toggle mic/cam or leave
final controller = ref.read(
meetingControllerProvider((token, meetingId, context)).notifier,
);
return WillPopScope(
onWillPop: () async {
controller.leaveMeeting();
return true;
},
child: Scaffold(
appBar: AppBar(title: const Text('VideoSDK QuickStart')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(meetingId),
// Render all participant
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
mainAxisExtent: 300,
),
itemCount: meetingState.participants.length,
itemBuilder: (context, index) {
final participants = meetingState.participants.values.toList();
return ParticipantTile(
key: Key(participants[index].id),
participant: participants[index],
videoStream: meetingState
.participantVideoStreams
[participants[index].id],
);
},
),
),
),
MeetingControls(
onToggleMicButtonPressed: controller.toggleMic,
onToggleCameraButtonPressed: controller.toggleCam,
onLeaveButtonPressed: controller.leaveMeeting,
isMicOff: meetingState.isMicOff,
isCamOff: meetingState.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.
If you get webrtc/webrtc.h file not found
error at a runtime in ios then check solution here.
You can checkout the complete flutter riverpod example here.
Got a Question? Ask us on discord