Costruire la UI della chat con StreamBuilder
Ora colleghiamo tutto a un widget. Alcune accortezze importanti:
- Il servizio va creato in
initState e chiuso in dispose, altrimenti i timer di riconnessione continuano a girare anche dopo che la schermata è stata smontata (classico memory leak).
- Accumuliamo i messaggi in una
List locale sottoscrivendo lo stream: StreamBuilder da solo mostrerebbe solo l'ultimo evento.
- Mostriamo un indicatore di stato nell'
AppBar usando il secondo stream.
- La
ListView è reverse: true così i nuovi messaggi compaiono in basso senza dover gestire manualmente lo scroll.
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
late final WebSocketService _service;
final _controller = TextEditingController();
final _messages = <ChatMessage>[];
StreamSubscription<ChatMessage>? _sub;
@override
void initState() {
super.initState();
_service = WebSocketService(Uri.parse('wss://echo.websocket.events'));
_sub = _service.chatMessages.listen((msg) {
if (!mounted) return;
setState(() => _messages.insert(0, msg));
});
_service.connect();
}
@override
void dispose() {
_sub?.cancel();
_service.dispose();
_controller.dispose();
super.dispose();
}
void _send() {
final text = _controller.text.trim();
if (text.isEmpty) return;
_service.sendChat(ChatMessage(
id: DateTime.now().microsecondsSinceEpoch.toString(),
author: 'io',
text: text,
sentAt: DateTime.now(),
));
_controller.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chat realtime'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(24),
child: StreamBuilder<SocketStatus>(
stream: _service.status,
initialData: _service.currentStatus,
builder: (context, snapshot) {
final status = snapshot.data ?? SocketStatus.disconnected;
final (label, color) = switch (status) {
SocketStatus.connected => ('Connesso', Colors.green),
SocketStatus.connecting => ('Connessione...', Colors.orange),
SocketStatus.disconnected => ('Offline', Colors.red),
};
return Container(
width: double.infinity,
color: color.withValues(alpha: 0.15),
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(label, textAlign: TextAlign.center),
);
},
),
),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
reverse: true,
padding: const EdgeInsets.all(12),
itemCount: _messages.length,
itemBuilder: (context, index) {
final msg = _messages[index];
return Align(
alignment: Alignment.centerRight,
child: Card(
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('${msg.author}: ${msg.text}'),
),
),
);
},
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
onSubmitted: (_) => _send(),
decoration: const InputDecoration(
hintText: 'Scrivi un messaggio...',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
IconButton.filled(
onPressed: _send,
icon: const Icon(Icons.send),
),
],
),
),
),
],
),
);
}
}
Risultato atteso
L'app mostra la chat: scrivendo un messaggio lo vedi comparire nella lista (rimandato indietro dall'echo server) e l'indicatore nell'AppBar segnala lo stato della connessione in tempo reale.