Channels
Subscribe to public, private and presence channels through the Echo facade and react to the events they deliver.
On this page
Basic Usage#
import 'package:magic_socket/magic_socket.dart';
void main() {
Echo.channel('orders').listen('OrderShipped', (event) {
print('Order ${event.data['id']} has shipped');
});
Echo.leave('orders');
}
Echo is a static facade over the BroadcastManager bound at broadcasting. It follows the shape of the Laravel Echo client (channel, private, join, listen and leave) with differences, the first being event names. With the null driver the calls succeed and no event ever arrives, so the same code runs in development without a server.
Quick Reference#
| Member | Returns | Meaning |
|---|---|---|
Echo.channel(name) |
BroadcastChannel |
A public channel; no auth. |
Echo.private(name) |
BroadcastChannel |
A private channel; the driver adds private- and authorises over HTTP. |
Echo.join(name) |
BroadcastPresenceChannel |
A presence channel; the driver adds presence- and tracks members. |
Echo.listen(channel, event, callback) |
BroadcastChannel |
Shorthand for Echo.channel(channel).listen(event, callback). |
Echo.leave(name) |
void |
Unsubscribes name and its private- and presence- forms, as Laravel Echo does: leave('chat.1') leaves private('chat.1'), join('chat.1') and a public chat.1. No effect when not subscribed. |
Echo.connect(), Echo.disconnect() |
Future<void> |
Opens or closes the socket, see Connection. |
channel.listen(event, callback) |
BroadcastChannel |
Registers a handler and returns the channel for chaining. |
channel.stopListening(event) |
void |
Removes the handler for that event. |
channel.events |
Stream<BroadcastEvent> |
Every event on the channel, whether or not a handler is registered. |
Event Names#
listen compares the name you pass with the event name in the frame, exactly. Echo JS prepends the event's namespace for you; this driver does not. Laravel names a frame by broadcastAs() when the event has one and by its full class name when it has none, so a handler registered for 'OrderShipped' never runs for a default App\Events\OrderShipped.
Name the event on the server and pass the same string to listen:
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class OrderShipped implements ShouldBroadcast
{
public function broadcastOn(): Channel
{
return new Channel('orders');
}
public function broadcastAs(): string
{
return 'OrderShipped';
}
}
import 'package:magic_socket/magic_socket.dart';
void main() {
Echo.channel('orders').listen('OrderShipped', (event) => print(event.data));
// Without broadcastAs() on the server, pass the full class name instead.
Echo.channel('orders').listen(r'App\Events\OrderShipped', (event) => print(event.data));
}
Echo's leading-dot form (.OrderShipped) is not needed and not recognised: pass the name as the frame carries it. Every other sample on this page assumes the event has a broadcastAs() equal to the name shown.
Public Channels#
Any connected client may subscribe to a public channel.
import 'package:magic_socket/magic_socket.dart';
void main() {
void onShipped(BroadcastEvent event) => print(event.data);
void onCancelled(BroadcastEvent event) => print(event.data);
Echo.channel('orders')
.listen('OrderShipped', onShipped)
.listen('OrderCancelled', onCancelled);
Echo.channel('orders').stopListening('OrderShipped');
}
A second listen for the same event on the same channel replaces the first handler. To fan one event out to several handlers, use BroadcastListeners.
Private Channels#
A private channel performs an HTTP handshake before it subscribes: the driver posts socket_id and channel_name to the auth_endpoint (/broadcasting/auth by default) through Http, and expects {"auth": "..."} back.
import 'package:magic_socket/magic_socket.dart';
void main() {
Echo.private('user.1').listen('ProfileUpdated', (event) {
print('Profile updated: ${event.data}');
});
}
The server decides who may join, in Laravel's routes/channels.php:
Broadcast::channel('user.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
The request goes through your Http stack, so the user's bearer token reaches the auth route like any other call. A refusal (401, 403) is logged and not retried; a missing response or a 5xx is retried with the reconnect backoff, see Connection.
Presence Channels#
A presence channel is a private channel that also tracks who is in it.
import 'package:magic_socket/magic_socket.dart';
void main() {
final room = Echo.join('room.1');
print('Online: ${room.members.length}');
room.onJoin.listen((member) => print('${member['name']} joined'));
room.onLeave.listen((member) => print('${member['name']} left'));
room.listen('MessagePosted', (event) => print(event.data['body']));
}
| Member | Type | Meaning |
|---|---|---|
members |
List<Map<String, dynamic>> |
The current members, an immutable snapshot. |
onJoin |
Stream<Map<String, dynamic>> |
Emits the member payload on each join. |
onLeave |
Stream<Map<String, dynamic>> |
Emits the member payload on each leave. |
The BroadcastEvent Envelope#
Every handler receives a BroadcastEvent.
| Property | Type | Meaning |
|---|---|---|
event |
String |
The event name as the server sent it: the event's broadcastAs(), or its full class name (App\Events\OrderShipped) when it has none. |
channel |
String |
The channel the event arrived on, with its private- or presence- prefix. |
data |
Map<String, dynamic> |
The decoded JSON payload. |
receivedAt |
DateTime |
The local time of receipt. |
Every frame the server sends reaches your handler, including one whose payload repeats an earlier frame. The server can also deliver an event twice when a queued broadcast job is retried. Write handlers so a second delivery is harmless: refetch, or upsert by an id in the payload, rather than counting or showing a toast per frame.
Subscription State#
A connected driver does not mean a subscribed channel: a private channel still waits on its auth call, and a refusal leaves it unsubscribed. Each channel reports the server's confirmation itself.
import 'package:magic_socket/magic_socket.dart';
void main() {
final channel = Echo.private('team.1');
print(channel.isSubscribed); // true once the server sent subscription_succeeded
channel.onSubscriptionChanged.listen((subscribed) {
print(subscribed ? 'stop polling' : 'start polling');
});
}
onSubscriptionChanged emits only on a change and never replays, so read isSubscribed when you attach. Channels of the null driver are never subscribed.
Gotchas#
| Mistake | Fix |
|---|---|
Listening for 'OrderShipped' when the server sends App\Events\OrderShipped |
The names must match exactly; add broadcastAs() to the event or pass the full class name, see Event Names. |
Using Echo.listen for a private channel |
It is shorthand for a public channel; call Echo.private(name).listen(...). |
| Registering two handlers for one event on one channel | The second replaces the first. Use BroadcastListeners to share the event. |
Treating onReconnect as "the channel is back" |
It fires after the subscribe was sent, not confirmed. Refetch on onSubscriptionChanged turning true. |
| Counting frames as unique events | The server can repeat a frame; make the handler idempotent. |
Calling Echo before Magic.init |
The broadcasting binding does not exist yet; await Magic.init in main. |
Expecting events from the null driver |
It drops everything; set BROADCAST_CONNECTION=reverb. |
Related Documentation#
- Configuration: connections, environment keys and the socket id header.
- Listeners: sharing a channel between controllers.
- Connection: lifecycle, reconnects and auth failures.
- Testing: faking channels and delivering events in a test.