Skip to main content

State Management

While events tell you what happened, ClassroomState tells you where you are in the classroom lifecycle. Use it to update your UI accordingly.

ClassroomState Enum​

StateMeaning
idleInitial state, not connected
connectingJoining the classroom
connectedActive in the classroom
reconnectingConnection dropped, trying to reconnect
endedInstructor ended the class
kickedUser was removed
errorSomething went wrong

State Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚
β”‚ idle ──▢ connecting ──▢ connected ◀──▢ reconnecting β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ended / kicked / β”‚ β”‚
β”‚ β”‚ error β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Tracking State​

Use the onStateChanged callback:

VerrifloPlayer(
iframeUrl: iframeUrl,
onStateChanged: (state) {
setState(() => _currentState = state);
},
)

Extension Methods​

ClassroomState has helper getters:

/// Is the classroom active (connected or reconnecting)?
state.isActive // true for connected, reconnecting

/// Has the classroom terminated (ended, kicked, or error)?
state.isTerminated // true for ended, kicked, error

/// Should we show a loading indicator?
state.isLoading // true for connecting, reconnecting

UI Patterns​

Loading Indicator​

Show loading during connection phases:


Widget build(BuildContext context) {
return Stack(
children: [
VerrifloPlayer(
iframeUrl: iframeUrl,
onStateChanged: (state) {
setState(() => _state = state);
},
),

// Overlay loading indicator
if (_state.isLoading)
Container(
color: Colors.black54,
child: const Center(
child: CircularProgressIndicator(),
),
),
],
);
}

State-Based UI​

Update your entire UI based on state:


Widget build(BuildContext context) {
switch (_state) {
case ClassroomState.idle:
case ClassroomState.connecting:
return _buildConnectingView();

case ClassroomState.connected:
return _buildConnectedView();

case ClassroomState.reconnecting:
return _buildReconnectingView();

case ClassroomState.ended:
return _buildEndedView();

case ClassroomState.kicked:
return _buildKickedView();

case ClassroomState.error:
return _buildErrorView();
}
}

Widget _buildConnectingView() {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Joining classroom...'),
],
),
);
}

Widget _buildConnectedView() {
return VerrifloPlayer(
iframeUrl: iframeUrl,
onStateChanged: (state) => setState(() => _state = state),
onClassEnded: () => setState(() => _state = ClassroomState.ended),
onKicked: (_) => setState(() => _state = ClassroomState.kicked),
);
}

Widget _buildReconnectingView() {
return Stack(
children: [
VerrifloPlayer(
iframeUrl: iframeUrl,
onStateChanged: (state) => setState(() => _state = state),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.orange,
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
SizedBox(width: 8),
Text(
'Reconnecting...',
style: TextStyle(color: Colors.white),
),
],
),
),
),
],
);
}

Widget _buildEndedView() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.videocam_off, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'Class Ended',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('The instructor has ended this session.'),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Back to Dashboard'),
),
],
),
);
}

Widget _buildKickedView() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.block, size: 64, color: Colors.red),
const SizedBox(height: 16),
const Text(
'Removed from Class',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('You have been removed by the instructor.'),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Back'),
),
],
),
);
}

Widget _buildErrorView() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
const Text(
'Something Went Wrong',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('Unable to connect to the classroom.'),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => setState(() => _state = ClassroomState.connecting),
child: const Text('Try Again'),
),
],
),
);
}

Simpler Approach: Let the Player Handle It​

The VerrifloPlayer already shows appropriate UI for most states:

  • Connecting: Loading spinner
  • Error: Error message with retry button
  • Ended: "Class Ended" overlay
  • Kicked: "Removed" overlay
  • Reconnecting: Orange banner

You can just handle navigation for terminal states:

VerrifloPlayer(
iframeUrl: iframeUrl,
onStateChanged: (state) {
// Only navigate away on terminal states
if (state == ClassroomState.ended) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const FeedbackPage()),
);
}
if (state == ClassroomState.kicked) {
Navigator.pop(context);
}
},
)

Combining State with Events​

State gives you the "where", events give you the "what":

class ClassroomScreen extends StatefulWidget {
final String iframeUrl;

const ClassroomScreen({required this.iframeUrl, super.key});


State<ClassroomScreen> createState() => _ClassroomScreenState();
}

class _ClassroomScreenState extends State<ClassroomScreen> {
ClassroomState _state = ClassroomState.idle;
String? _kickReason;
int _participantCount = 0;


Widget build(BuildContext context) {
return Scaffold(
appBar: _state.isActive ? AppBar(
title: Text('Live Class ($_participantCount watching)'),
) : null,

body: VerrifloPlayer(
iframeUrl: widget.iframeUrl,

onStateChanged: (state) {
setState(() => _state = state);
},

onEvent: (event) {
// Track specific events
switch (event.type) {
case VerrifloEventType.participantJoined:
setState(() => _participantCount++);
break;
case VerrifloEventType.participantLeft:
setState(() => _participantCount--);
break;
case VerrifloEventType.participantKicked:
_kickReason = event.reason;
break;
default:
break;
}
},

onClassEnded: () => _navigateToFeedback(),
onKicked: (reason) => _showKickedDialog(reason),
),
);
}

void _navigateToFeedback() {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const FeedbackPage()),
);
}

void _showKickedDialog(String? reason) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Removed'),
content: Text(reason ?? 'You were removed from this class.'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // Close dialog
Navigator.pop(context); // Leave screen
},
child: const Text('OK'),
),
],
),
);
}
}

Next: Video Quality β€” Control streaming quality.