Skip to main content
Version: 2.x.x

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.
important

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 iOSWeb (Beta)Desktop (Beta)Safari

Structure of the project

Your project structure should look like this.

Project Structure
    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.

VideoSDK Flutter Quick Start Architecture

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.
AndroidManifest.xml
<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 :
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.

api_call.dart
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.

meeting_controller.dart
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.
meeting_state.dart
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
tip

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.
join_screen.dart
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 .
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
meeting_controls.dart
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.
participant_tile.dart
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 using token, meetingId, and context.
  • Watches the provider for MeetingState to update UI reactively.
  • Reads the provider notifier to invoke meeting control actions.
meeting_screen.dart
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.


caution

If you get webrtc/webrtc.h file not found error at a runtime in ios then check solution here.

tip

You can checkout the complete flutter riverpod example here.

Got a Question? Ask us on discord