Skip to main content

State Management

React to state changes in your UI.

Using QuickRTCConference (Simplest)

The QuickRTCConference widget handles state management for you:

QuickRTCConference(
serverUrl: 'https://your-server.com:3000',
conferenceId: 'room-123',
participantName: 'Alice',
builder: (context, state, controller) {
// state and controller are provided directly
return Text('${state.participantCount} participants');
},
)

Simple Approach: ListenableBuilder

The controller extends ChangeNotifier, so use Flutter's built-in ListenableBuilder:

ListenableBuilder(
listenable: controller,
builder: (context, _) {
final state = controller.state;
return Text('${state.participantCount} participants');
},
)

This is the recommended approach for simple apps.

Advanced Approach: QuickRTC Widgets

For larger apps, use the provider-based widgets.

QuickRTCProvider

Provides the controller to descendants:

QuickRTCProvider(
controller: controller,
child: MyConferenceScreen(),
)

// Access in descendants:
final controller = QuickRTCProvider.of(context);

QuickRTCBuilder

Rebuilds when state changes (requires provider ancestor):

QuickRTCBuilder(
buildWhen: (prev, curr) => prev.participantCount != curr.participantCount,
builder: (context, state) {
return Text('${state.participantCount} participants');
},
)

QuickRTCListener

Handles side effects (navigation, snackbars):

QuickRTCListener(
listenWhen: (prev, curr) => prev.error != curr.error,
listener: (context, controller) {
if (controller.state.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(controller.state.error!)),
);
}
},
child: MyWidget(),
)

QuickRTCConsumer

Combines Provider + Listener + Builder:

QuickRTCConsumer(
controller: controller, // Optional if inside a QuickRTCProvider
listener: (context, controller) {
if (controller.state.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(controller.state.error!)),
);
}
},
builder: (context, state, controller) {
return VideoGrid(participants: state.participantList);
},
)

State Properties

final state = controller.state;

// Connection
state.isConnected
state.conferenceId
state.participantId
state.participantName

// Local media presence
state.hasLocalAudio
state.hasLocalVideo
state.hasLocalScreenshare

// Local media paused state
state.isLocalAudioPaused
state.isLocalVideoPaused
state.isLocalScreensharePaused

// Local media streams
state.localAudioStream
state.localVideoStream
state.localScreenshareStream

// Remote participants
state.participantList // List<RemoteParticipant>
state.participantCount // int

// Errors
state.hasError
state.error

Convenience Getters

These combine presence and paused state for easier UI logic:

// Active = present AND not paused
state.isLocalAudioActive // hasLocalAudio && !isLocalAudioPaused
state.isLocalVideoActive // hasLocalVideo && !isLocalVideoPaused
state.isLocalScreenshareActive // hasLocalScreenshare && !isLocalScreensharePaused

Before (verbose):

final isAudioOn = state.hasLocalAudio && !state.isLocalAudioPaused;
final isVideoOn = state.hasLocalVideo && !state.isLocalVideoPaused;

IconButton(
icon: Icon(isAudioOn ? Icons.mic : Icons.mic_off),
onPressed: () => controller.toggleMicrophoneMute(),
)

After (cleaner):

IconButton(
icon: Icon(state.isLocalAudioActive ? Icons.mic : Icons.mic_off),
onPressed: () => controller.toggleMicrophoneMute(),
)

RemoteParticipant

for (final p in state.participantList) {
p.id // String
p.name // String
p.videoStream // RemoteStream?
p.audioStream // RemoteStream?
p.screenshareStream // RemoteStream?
p.hasVideo // bool
p.hasAudio // bool
p.hasScreenshare // bool
p.isVideoMuted // bool
p.isAudioMuted // bool
}

Error Handling

if (state.hasError) {
print(state.error);
controller.clearError();
}

Complete Example with Convenience Getters

class ConferenceControls extends StatelessWidget {
final QuickRTCController controller;
final QuickRTCState state;


Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Using convenience getters for cleaner code
_ControlButton(
icon: state.isLocalAudioActive ? Icons.mic : Icons.mic_off,
active: state.isLocalAudioActive,
onTap: () => controller.toggleMicrophoneMute(),
),
_ControlButton(
icon: state.isLocalVideoActive ? Icons.videocam : Icons.videocam_off,
active: state.isLocalVideoActive,
onTap: () => controller.toggleCameraPause(),
),
_ControlButton(
icon: state.isLocalScreenshareActive
? Icons.stop_screen_share
: Icons.screen_share,
active: state.isLocalScreenshareActive,
onTap: () => controller.toggleScreenShareWithPicker(context),
),
_ControlButton(
icon: Icons.call_end,
active: false,
destructive: true,
onTap: () => controller.leaveMeeting(),
),
],
);
}
}