Flutter: Creando un Cliente de Chat en Tiempo Real con WebSockets y Provider
Este tutorial te guiará en la creación de una aplicación de chat en tiempo real con Flutter. Exploraremos cómo implementar la comunicación bidireccional utilizando WebSockets y gestionaremos el estado de la aplicación de manera eficiente con el paquete Provider, permitiéndote construir interfaces de usuario reactivas y fluidas.
🚀 Introducción al Chat en Tiempo Real con Flutter y WebSockets
En la era digital actual, las aplicaciones de mensajería y chat en tiempo real son omnipresentes. Desde las redes sociales hasta las herramientas de colaboración empresarial, la capacidad de comunicarse instantáneamente es fundamental. Flutter, con su rendimiento excepcional y su flexibilidad para construir interfaces de usuario atractivas, es una plataforma ideal para desarrollar este tipo de aplicaciones.
En este tutorial, nos embarcaremos en el emocionante viaje de construir un cliente de chat en tiempo real desde cero. Utilizaremos WebSockets para establecer una conexión persistente y bidireccional entre nuestro cliente Flutter y un servidor (que simularemos o te daremos una base para conectar). Para una gestión de estado robusta y escalable, integraremos el popular paquete Provider.
¿Por qué WebSockets para el Chat en Tiempo Real? 🤔
HTTP, el protocolo estándar para la web, es stateless y request-response. Esto significa que un cliente envía una solicitud y el servidor responde, cerrando la conexión. Para el chat, donde los mensajes deben fluir en ambas direcciones en cualquier momento, HTTP sería ineficiente (requeriría polling constante).
WebSockets, por otro lado, establecen una conexión persistente y bidireccional sobre una única conexión TCP. Una vez establecida, tanto el cliente como el servidor pueden enviar datos en cualquier momento sin necesidad de reestablecer la conexión. Esto lo hace ideal para aplicaciones que requieren baja latencia y comunicación full-duplex como el chat, notificaciones push, juegos en línea, etc.
Gestión de Estado con Provider ✨
Provider es una de las soluciones de gestión de estado más populares y recomendadas en Flutter. Es simple, escalable y muy eficiente. Nos permite:
- Exponer datos a través del árbol de widgets.
- Reconstruir selectivamente solo los widgets que dependen de esos datos cuando cambian.
- Separar la lógica de negocio de la interfaz de usuario.
Utilizaremos Provider para gestionar la lista de mensajes, el estado de la conexión WebSocket y la información del usuario.
🛠️ Configuración del Entorno y Dependencias
Antes de sumergirnos en el código, necesitamos configurar nuestro proyecto Flutter y añadir las dependencias necesarias.
1. Crear un Nuevo Proyecto Flutter
Si aún no tienes uno, crea un nuevo proyecto Flutter desde tu terminal:
flutter create flutter_websocket_chat
cd flutter_websocket_chat
2. Añadir Dependencias 📦
Necesitaremos dos paquetes principales:
web_socket_channel: Para la comunicación con WebSockets.provider: Para la gestión de estado. \Abre tu archivopubspec.yamly añade las siguientes líneas bajodependencies:
dependencies:
flutter:
sdk: flutter
web_socket_channel: ^2.4.0
provider: ^6.0.5
Luego, ejecuta flutter pub get para descargar los paquetes:
flutter pub get
🔌 Implementando la Conexión WebSocket
Crearemos una clase WebSocketService que encapsulará toda la lógica relacionada con la conexión y comunicación WebSocket.
1. Definir el Modelo de Mensaje 💬
Primero, necesitamos un modelo simple para nuestros mensajes. Crea un archivo lib/models/message.dart:
// lib/models/message.dart
import 'package:flutter/foundation.dart';
class Message {
final String sender;
final String content;
final DateTime timestamp;
Message({
required this.sender,
required this.content,
required this.timestamp,
});
factory Message.fromJson(Map<String, dynamic> json) {
return Message(
sender: json['sender'] as String,
content: json['content'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
);
}
Map<String, dynamic> toJson() => {
'sender': sender,
'content': content,
'timestamp': timestamp.toIso8601String(),
};
@override
String toString() {
return 'Message{sender: $sender, content: $content, timestamp: $timestamp}';
}
}
2. Crear WebSocketService 🌐
Ahora, implementemos el servicio WebSocket. Crea un archivo lib/services/websocket_service.dart:
// lib/services/websocket_service.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter_websocket_chat/models/message.dart';
enum WebSocketStatus {
connecting,
connected,
disconnected,
error,
}
class WebSocketService extends ChangeNotifier {
WebSocketChannel? _channel;
final String _url;
WebSocketStatus _status = WebSocketStatus.disconnected;
final List<Message> _messages = [];
String? _username;
WebSocketService(this._url);
WebSocketStatus get status => _status;
List<Message> get messages => _messages;
String? get username => _username;
void setUsername(String name) {
_username = name;
notifyListeners();
}
Future<void> connect() async {
if (_status == WebSocketStatus.connected || _username == null) return;
_status = WebSocketStatus.connecting;
notifyListeners();
print('Attempting to connect to $_url');
try {
_channel = WebSocketChannel.connect(Uri.parse(_url));
await _channel!.ready; // Espera a que la conexión esté lista
_status = WebSocketStatus.connected;
print('WebSocket connected.');
notifyListeners();
_channel!.stream.listen(
(data) {
print('Received: $data');
try {
final Map<String, dynamic> json = jsonDecode(data as String);
final message = Message.fromJson(json);
_messages.add(message);
notifyListeners();
} catch (e) {
print('Error decoding message: $e');
}
},
onDone: () {
print('WebSocket disconnected.');
_status = WebSocketStatus.disconnected;
notifyListeners();
// Opcional: intentar reconectar
// Future.delayed(Duration(seconds: 5), () => connect());
},
onError: (error) {
print('WebSocket error: $error');
_status = WebSocketStatus.error;
notifyListeners();
disconnect(); // Cierra la conexión en caso de error
},
cancelOnError: true,
);
} catch (e) {
print('Failed to connect to WebSocket: $e');
_status = WebSocketStatus.error;
notifyListeners();
}
}
void sendMessage(String content) {
if (_status == WebSocketStatus.connected && _username != null) {
final message = Message(
sender: _username!,
content: content,
timestamp: DateTime.now(),
);
final jsonMessage = jsonEncode(message.toJson());
_channel!.sink.add(jsonMessage);
print('Sent: $jsonMessage');
} else {
print('Cannot send message. Not connected or username not set.');
}
}
void disconnect() {
if (_channel != null && _status != WebSocketStatus.disconnected) {
_channel!.sink.close();
_status = WebSocketStatus.disconnected;
notifyListeners();
print('WebSocket closed.');
}
}
@override
void dispose() {
disconnect();
super.dispose();
}
}
Vamos a desglosar este servicio:
_url: La dirección de nuestro servidor WebSocket._channel: La instancia deWebSocketChannelque gestiona la conexión._status: Unenumpara rastrear el estado actual de la conexión._messages: Una lista para almacenar los mensajes recibidos._username: El nombre del usuario actual, necesario para enviar mensajes.connect(): Inicializa la conexión WebSocket, escucha los mensajes entrantes y maneja los eventos de cierre o error._channel!.stream.listen(): Este es el corazón de la recepción de mensajes. Cuando llega un mensaje, se decodifica de JSON a nuestro objetoMessagey se añade a la lista.
sendMessage(): Envía un mensaje al servidor después de serializarlo a JSON.disconnect(): Cierra la conexión WebSocket de forma segura.dispose(): Importante para liberar recursos cuando el servicio ya no es necesario.
🧩 Integración con Provider y la Interfaz de Usuario
Ahora que tenemos nuestro servicio WebSocket, vamos a integrarlo con Provider para construir la interfaz de usuario del chat.
1. Configurar Provider en main.dart 🌳
Envuelve tu aplicación con ChangeNotifierProvider para que WebSocketService esté disponible en todo el árbol de widgets. Abre lib/main.dart y modifícalo:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_websocket_chat/services/websocket_service.dart';
import 'package:flutter_websocket_chat/screens/chat_screen.dart';
import 'package:flutter_websocket_chat/screens/username_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => WebSocketService('ws://echo.websocket.events'), // ¡Cambia esta URL a tu servidor!
child: MaterialApp(
title: 'Flutter WebSocket Chat',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const UsernameScreen(),
),
);
}
}
2. Pantalla de Entrada de Nombre de Usuario 👋
Necesitamos una pantalla para que el usuario ingrese su nombre antes de unirse al chat. Crea lib/screens/username_screen.dart:
// lib/screens/username_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_websocket_chat/services/websocket_service.dart';
import 'package:flutter_websocket_chat/screens/chat_screen.dart';
class UsernameScreen extends StatefulWidget {
const UsernameScreen({super.key});
@override
State<UsernameScreen> createState() => _UsernameScreenState();
}
class _UsernameScreenState extends State<UsernameScreen> {
final TextEditingController _usernameController = TextEditingController();
@override
void dispose() {
_usernameController.dispose();
super.dispose();
}
void _joinChat() {
final username = _usernameController.text.trim();
if (username.isNotEmpty) {
Provider.of<WebSocketService>(context, listen: false).setUsername(username);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const ChatScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Bienvenido al Chat'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Ingresa tu nombre de usuario',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _joinChat(),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _joinChat,
child: const Text('Unirse al Chat'),
),
],
),
),
);
}
}
3. Pantalla Principal del Chat 💬
Esta será la pantalla donde se mostrarán los mensajes y el usuario podrá enviar los suyos. Crea lib/screens/chat_screen.dart:
// lib/screens/chat_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_websocket_chat/services/websocket_service.dart';
import 'package:intl/intl.dart'; // Para formatear la hora, añade intl a pubspec.yaml si no lo tienes
import 'package:flutter_websocket_chat/models/message.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
// Conectar al WebSocket cuando la pantalla se inicializa
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<WebSocketService>(context, listen: false).connect();
});
}
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
// Desconectar al salir de la pantalla
Provider.of<WebSocketService>(context, listen: false).disconnect();
super.dispose();
}
void _sendMessage() {
final content = _messageController.text.trim();
if (content.isNotEmpty) {
Provider.of<WebSocketService>(context, listen: false).sendMessage(content);
_messageController.clear();
_scrollToBottom();
}
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Consumer<WebSocketService>(
builder: (context, service, child) {
String titleText = 'Chat';
Color statusColor = Colors.grey;
switch (service.status) {
case WebSocketStatus.connected:
titleText = 'Chat (${service.username ?? 'Anónimo'}) - Conectado';
statusColor = Colors.green;
break;
case WebSocketStatus.connecting:
titleText = 'Chat (${service.username ?? 'Anónimo'}) - Conectando...';
statusColor = Colors.orange;
break;
case WebSocketStatus.disconnected:
titleText = 'Chat (${service.username ?? 'Anónimo'}) - Desconectado';
statusColor = Colors.red;
break;
case WebSocketStatus.error:
titleText = 'Chat (${service.username ?? 'Anónimo'}) - Error';
statusColor = Colors.deepOrange;
break;
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(titleText),
Icon(Icons.circle, color: statusColor, size: 12),
],
);
},
),
),
body: Column(
children: [
Expanded(
child: Consumer<WebSocketService>(
builder: (context, service, child) {
_scrollToBottom(); // Asegura que se desplace al final al recibir nuevos mensajes
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(8.0),
itemCount: service.messages.length,
itemBuilder: (context, index) {
final message = service.messages[index];
final isMe = message.sender == service.username;
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4.0),
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration(
color: isMe ? Colors.blueAccent : Colors.grey[300],
borderRadius: BorderRadius.circular(12.0),
),
child: Column(
crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Text(
isMe ? 'Tú' : message.sender,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isMe ? Colors.white : Colors.black,
),
),
const SizedBox(height: 4.0),
Text(
message.content,
style: TextStyle(
color: isMe ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 4.0),
Text(
DateFormat('HH:mm').format(message.timestamp),
style: TextStyle(
fontSize: 10.0,
color: isMe ? Colors.white70 : Colors.black54,
),
),
],
),
),
);
},
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: 'Escribe un mensaje...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
),
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 8.0),
Consumer<WebSocketService>(
builder: (context, service, child) {
return FloatingActionButton(
mini: true,
onPressed: service.status == WebSocketStatus.connected ? _sendMessage : null,
child: const Icon(Icons.send),
backgroundColor: service.status == WebSocketStatus.connected ? Colors.blue : Colors.grey,
);
},
),
],
),
),
],
),
);
}
}
Si aún no lo tienes, añade intl a tu pubspec.yaml para formatear las fechas:
dependencies:
flutter:
sdk: flutter
web_socket_channel: ^2.4.0
provider: ^6.0.5
intl: ^0.18.1 # Añade esta línea
Luego flutter pub get.
En ChatScreen:
initState: Llamamos aconnect()en elWebSocketServicecuando la pantalla se inicializa. UsamosWidgetsBinding.instance.addPostFrameCallbackpara asegurar que elBuildContextesté completamente disponible.dispose: Aseguramos que la conexión WebSocket se cierre cuando la pantalla se descarte._sendMessage: Llama al métodosendMessagedelWebSocketServicey limpia el campo de texto._scrollToBottom: Una función para mantener la lista de mensajes desplazada automáticamente hacia el último mensaje.Consumer<WebSocketService>: Este widget de Provider nos permite escuchar los cambios enWebSocketServicey reconstruir solo las partes de la UI que dependen de él.- El título de la
AppBarmuestra el estado de la conexión. - El
ListView.builderse encarga de mostrar todos los mensajes, diferenciando si el mensaje es del usuario actual o de otro. - El botón de envío de mensajes se habilita/deshabilita según el estado de la conexión.
- El título de la
💡 Sugerencia de Servidor WebSocket Simple (Node.js)
Para probar tu aplicación de chat de forma real, necesitarás un servidor WebSocket que retransmita los mensajes. Aquí hay un ejemplo básico con Node.js y la librería ws:
- Crea un nuevo directorio para tu servidor (fuera de tu proyecto Flutter).
- Inicializa un proyecto Node.js:
npm init -y
- Instala la librería
ws:
npm install ws
- Crea un archivo
server.js:
// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', ws => {
console.log('Cliente conectado');
ws.on('message', message => {
console.log(`Recibido: ${message}`);
// Retransmitir el mensaje a todos los clientes conectados
wss.clients.forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
} else if (client === ws && client.readyState === WebSocket.OPEN) {
// También podemos enviar el mensaje de vuelta al remitente para confirmación
client.send(message);
}
});
});
ws.on('close', () => {
console.log('Cliente desconectado');
});
ws.on('error', error => {
console.error('Error del WebSocket:', error);
});
// Opcional: Enviar un mensaje de bienvenida al nuevo cliente
ws.send(JSON.stringify({
sender: 'Servidor',
content: '¡Bienvenido al chat!',
timestamp: new Date().toISOString()
}));
});
console.log('Servidor WebSocket iniciado en ws://localhost:8080');
- Inicia el servidor:
node server.js
Ahora, cambia la URL en main.dart a ws://localhost:8080 (o la IP de tu máquina si pruebas en un dispositivo físico).
✅ Probando la Aplicación
- Asegúrate de que tu servidor WebSocket esté en ejecución.
- Ejecuta tu aplicación Flutter:
flutter run
- Ingresa un nombre de usuario en la pantalla de bienvenida.
- Accede a la pantalla de chat. Deberías ver el estado de conexión como "Conectado" (punto verde).
- Envía mensajes. Si usas el servidor Node.js, deberías poder abrir la aplicación en dos dispositivos o emuladores diferentes, cada uno con un nombre de usuario distinto, y ver cómo los mensajes se intercambian en tiempo real.
Consideraciones para Producción ⚠️
Para una aplicación de producción, necesitarías añadir:
- Autenticación y Autorización: Para asegurar que solo usuarios válidos puedan chatear.
- Persistencia de Mensajes: Guardar el historial de chat en una base de datos.
- Manejo de Errores Robusto: Reconexión automática con backoff exponencial.
- Seguridad: Usar
wss://(WebSockets seguros) con certificados TLS. - Escalabilidad: Un servidor WebSocket más robusto capaz de manejar miles de conexiones simultáneas (ej.
Socket.IO,wscon Node.js en producción,Go,ElixirconPhoenix). - UI/UX mejorada: Indicadores de escritura, notificaciones, etc.
🎯 Conclusión
¡Felicidades! 🎉 Has construido un cliente de chat en tiempo real completamente funcional en Flutter utilizando WebSockets para la comunicación bidireccional y Provider para una gestión de estado eficiente. Has aprendido a:
- Configurar un proyecto Flutter con las dependencias necesarias (
web_socket_channel,provider,intl). - Diseñar un modelo de datos para los mensajes.
- Implementar un servicio WebSocket (
WebSocketService) que maneja la conexión, el envío y la recepción de mensajes. - Integrar
WebSocketServiceconProviderpara hacer que los datos y el estado de la conexión sean accesibles en toda la aplicación. - Crear una interfaz de usuario de chat reactiva que muestra mensajes en tiempo real y permite al usuario enviar nuevos mensajes.
- Configurar un servidor WebSocket básico con Node.js para pruebas.
Este tutorial te proporciona una base sólida para desarrollar aplicaciones de comunicación en tiempo real más complejas. La combinación de Flutter, WebSockets y Provider es poderosa y te abrirá un mundo de posibilidades para crear experiencias de usuario interactivas y dinámicas.
Tutoriales relacionados
- Flutter: Desarrollando un Sistema de Autenticación Completo con Firebase Auth y Providerintermediate20 min
- Flutter: Integrando Modelos de IA en tu Aplicación Móvil con TensorFlow Liteintermediate20 min
- Flutter para Principiantes: Construyendo una Lista de Tareas Interactivas y Persistentes con Isar DBbeginner30 min
- Flutter Avanzado: Sincronización de Datos Offline con WorkManager y Hive para Apps Robuadvanced20 min
- Flutter para Principiantes: Creando Interfaces Adaptativas y Responsivas para Cualquier Pantallabeginner18 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!