devShakib

cross_tab

Coordinate the browser tabs of a Flutter Web app: send typed messages between them, know how many are open, and elect exactly one to do the work.

cross_tab

Coordinate the browser tabs of a Flutter Web app.

final tabs = CrossTab.open('my-app');tabs.presence.listen((p) {  if (p.isLeader) {    startPolling();  } else {    stopPolling();  }});tabs.messages.listen((m) => print('${m.from} said ${m.data}'));tabs.send({'signedOut': true});

The problem

Open your app in two tabs and things start going wrong quietly. Both poll the

server. Both hold a socket. Both raise the same notification. The user signs

out in one and the other carries on as though nothing happened.

The browser gives you BroadcastChannel and stops there. What is missing is

everything around it.

| what you need | what the platform gives you |

| --- | --- |

| messages with a sender | untyped structured clones, no identity |

| how many tabs are open | nothing — you maintain it by heartbeat |

| exactly one tab doing the work | nothing at all |

A leader

This is the part worth the package. Exactly one tab should poll, hold the

socket, or raise the notification — and when that tab closes, another must pick

it up without being told.

tabs.presence.listen((p) {  if (p.isLeader) {    socket.connect();  } else {    socket.disconnect();  }});

The oldest tab leads, with the id as a tiebreak. That second part matters more

than it looks: two tabs opened in the same microsecond would otherwise each

conclude the other should lead, or both lead. Comparing ids makes the ordering

total, so every tab reaches the same answer without further negotiation.

A tab that closes says goodbye, so the others react at once rather than waiting

out a timeout. A tab that crashes says nothing, so it is dropped after three

missed heartbeats.

Presence

final p = tabs.current;p.count;     // how many tabs are openp.tabs;      // their ids, including this onep.leader;    // whose turn it is, or null while an election settlesp.isLeader;  // whether that is you

presence emits the current state on subscription, so a listener knows where

it stands immediately rather than after the next change.

Messages

tabs.send({'cart': items.length});tabs.messages.listen((m) => print('from ${m.from}: ${m.data}'));

A tab never receives its own messages — the sender already knows what it sent.

Off the web

The package compiles everywhere. On mobile and desktop there is exactly one

instance of your app, so it reports one tab and is always the leader.

That is the correct answer rather than a degraded one, and it is deliberate: it

means the code above runs unchanged on every platform, with no if (kIsWeb)

wrapped around your leader logic.

Verified

Two BroadcastChannel objects with the same name see each other even within

one document, so the multi-tab behaviour is exercised for real rather than

mocked. flutter test --platform chrome covers four tabs agreeing on a single

leader, the oldest tab winning, leadership passing when the leader closes,

tabs discovering and losing each other, and a tab not hearing its own

messages. The single-instance path is tested on the VM.

License

MIT © K M Shahriar Hossain

cross_tab on pub.dev