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β
| State | Meaning |
|---|---|
idle | Initial state, not connected |
connecting | Joining the classroom |
connected | Active in the classroom |
reconnecting | Connection dropped, trying to reconnect |
ended | Instructor ended the class |
kicked | User was removed |
error | Something 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.