Skip to main content
Version: 4.x.x

Migration Guide

This guide covers what you need to change when upgrading the Flutter SDK from v3.x.x to v4.0.1. Most of the work is in PubSub — everything else is two corrections to the error catalog. (v4.0.0 was retracted; upgrade straight to v4.0.1.)

Nothing stops compiling: publish(), subscribe() and unsubscribe() keep the signatures they had in v3.x.x, and the new options and callbacks are optional named parameters. The behaviour changes, though, and section 1 needs a catch at every call site.

The batching, streamed history and options below take effect on rooms served by VideoSDK's new PubSub service. The SDK selects it automatically and the API is identical either way; on a room that falls back to the previous service, subscribe() options are ignored, onBatchReceived and onMessageDrop never fire, and all history arrives in a single onOldMessagesReceived call with isLast set to true. Failures throw and reach Events.error on both.


1. PubSub methods now throw

publish(), subscribe() and unsubscribe() throw a PubSubException when the call does not take effect. In v3.x.x they failed silently — the future completed and a rejected publish() looked exactly like a successful one.

Before

// A rejected publish looked exactly like a successful one.
room.pubSub.publish("CHAT", message, const PubSubPublishOptions(persist: true));

After

try {
await room.pubSub.publish("CHAT", message, const PubSubPublishOptions(persist: true));
msgTextController.clear();
} catch (e) {
// The message did not reach the room — leave the text in place to retry.
showSnackBar("Not sent: $e");
}

dispose() cannot await, so unsubscribe with .catchError there:

@override
void dispose() {
room.pubSub
.unsubscribe("CHAT", messageHandler)
.catchError((Object e) => log("Unsubscribe failed: $e"));
super.dispose();
}

Key points:

  • Failures now surface at the call site as well as through Events.error. The event still carries the same failure, so use it for centralized logging and catch for handling one call.
  • Dart cannot check this at compile time. An unawaited onPressed: () => room.pubSub.publish(...) turns a rejection into an unhandled async error, so audit every pubSub. call site before shipping.
  • unsubscribe() detaches your handler before anything that can throw, so a dispose() teardown always completes.
  • A failed subscribe() leaves nothing behind: the SDK takes your handler back out, so you will not receive messages for a topic whose subscribe was rejected.
  • subscribe() returns Future<PubSubMessages>, so a .catchError on it must return a value — PubSubMessages(messages: const []) will do.
  • publish() and unsubscribe() resolve only once the server has answered, bounded by a 30s timeout, so a caught exception always refers to the call you awaited.

2. Typed PubSub exceptions

PubSubException is sealed, so you can switch over the exact subtype instead of matching on strings, and the analyzer tells you when a case is missing. Each subtype carries code, name and message, where message is the reason the server gave.

After

try {
await room.pubSub.publish("CHAT", message);
} on PubSubException catch (e) {
switch (e) {
case PubSubMeetingNotJoined():
showToast("Join the meeting before publishing");
case PubSubMeetingReconnecting():
showToast("Reconnecting — try again shortly");
case PubSubPublishFailed():
markUnsent(id, e.message);
default:
log(e.toString());
}
}
ExceptionNameCode
PubSubPublishFailedPUBSUB_PUBLISH_FAILED4087
PubSubSubscribeFailedPUBSUB_SUBSCRIBE_FAILED4088
PubSubUnsubscribeFailedPUBSUB_UNSUBSCRIBE_FAILED4089
PubSubMeetingNotJoinedERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED3022
PubSubMeetingReconnectingERROR_MEETING_RECONNECTING3027

Key points:

  • A call that times out throws its method's exception with the reason in message, rather than a separate type.
  • name is the same constant iOS returns from VideoSDKError.type, which is useful when correlating logs across platforms.
  • The last two are meeting-state failures that already reached Events.error in v3.12.0; in v4.0.1 they throw as well.
  • Not every failure can throw — a subscription the server drops later has no call left to throw from and arrives as PUBSUB_SUBSCRIBE_FAILED on Events.error only.
  • If you already log every Events.error, expect new entries on rooms where these calls were quietly failing before.
  • Learn more: Error Events

3. History arrives through onOldMessagesReceived

In v3.x.x, subscribe() returned a PubSubMessages holding every persisted message for the topic, in one piece. History is now streamed in batches, oldest first, through the new onOldMessagesReceived callback, so a long conversation starts rendering immediately instead of waiting for the whole backlog.

Before

room.pubSub
.subscribe("CHAT", messageHandler)
.then((value) => setState(() => messages = value));

After

room.pubSub.subscribe(
"CHAT",
messageHandler,
onOldMessagesReceived: (batch, isLast) {
setState(() => messages.addAll(batch));
if (isLast) {
// Every persisted message for this topic has now been delivered.
scrollToBottom();
}
},
);

Key points:

  • History arrives in multiple batches. Accumulate them and use isLast to know when the final one has landed.
  • The return value's meaning depends on options: with none, subscribe() still resolves with the complete history as in v3.x.x; as soon as you pass options it resolves on the first batch, so treat onOldMessagesReceived as the source of truth.
  • Use oldMessageLimit (see section 5) to cap how much history you fetch, or pass 0 to skip it entirely.

4. New subscribe() callbacks

Alongside onOldMessagesReceived, subscribe() accepts two more optional callbacks. All three are named parameters with no default behaviour, so a v3.x.x call that passes only a message handler is unchanged.

room.pubSub.subscribe(
"CHAT",
// Existing — fires once per live message.
(PubSubMessage message) { },

// New — persisted messages, streamed oldest first.
onOldMessagesReceived: (List<PubSubMessage> messages, bool isLast) { },

// New — the same live messages, grouped into a batch.
onBatchReceived: (List<PubSubMessage> messages) { },

// New — how many live messages could not be delivered to you.
onMessageDrop: (int droppedCount) { },
);

Key points:

  • On a busy topic, prefer onBatchReceived over the per-message handler — one setState() per batch instead of one per message. They deliver the same messages, so use one or the other, not both.
  • onMessageDrop reports a count, not the messages themselves. It fires when a subscriber fell too far behind, when a newMessageLimit cap skipped messages, and after a reconnect for anything that arrived while you were offline.
  • Under PubSubRealtimeOverflow.drop the SDK does not track sequence numbers, so onMessageDrop never fires and the loss is uncounted.

5. New subscribe() options

subscribe() accepts an optional options parameter of type PubSubSubscribeOptions, which controls how much history you fetch and what happens when messages arrive faster than your app can drain them.

room.pubSub.subscribe(
"CHAT",
messageHandler,
options: const PubSubSubscribeOptions(
oldMessageLimit: 100,
realtimeOverflow: PubSubRealtimeOverflow.queue,
maxQueue: 70,
),
onOldMessagesReceived: handleOldMessages,
);

Key points:

  • oldMessageLimitnull (default) replays all history, 0 replays none, N replays the last N. Only messages published with persist: true are stored at all.
  • realtimeOverflowqueue (default) briefly holds messages for a subscriber that falls behind so it can catch up; drop discards them. Use drop only where stale data is worthless, such as typing indicators.
  • maxQueue — how far a subscriber may fall behind, counted in delivery batches. Only meaningful with queue, and fixed by whichever subscriber reaches the topic first.
  • newMessageLimit — caps how many live messages you accept per server batching window. null or 0 (default) means unlimited.

Options are checked before anything is sent. A value that cannot work throws an ArgumentError — the same four rules iOS and Android reject:

RuleExample that throws
oldMessageLimit must be >= 0, or null for all historyoldMessageLimit: -1
maxQueue must be >= 1, or null for the server defaultmaxQueue: 0
newMessageLimit must be >= 0, or null for unlimitednewMessageLimit: -5
maxQueue cannot be combined with PubSubRealtimeOverflow.dropmaxQueue: 70 with realtimeOverflow: PubSubRealtimeOverflow.drop

An ArgumentError is a mistake in the call, not a failure of it — so it is not a PubSubException, on PubSubException catch will not catch it, and it never reaches Events.error. Fix the value rather than handling it.


6. Subscriptions survive a reconnect

In v3.x.x a PubSub subscription lived only as long as the connection, so apps that wanted to keep receiving messages after a network drop had to re-subscribe from a reconnect handler. The SDK now restores every active subscription itself.

Before

room.on(Events.roomStateChanged, (RoomState state) {
if (state == RoomState.connected) {
// Re-subscribing by hand, or messages stopped arriving.
room.pubSub.subscribe("CHAT", messageHandler);
}
});

After

room.on(Events.roomStateChanged, (RoomState state) {
if (state == RoomState.connected) {
// Nothing to do — the SDK has already restored the subscription.
}
});

Key points:

  • Remove your manual re-subscribe. Every subscribe() opens its own subscription, so calling it again after the SDK restored yours leaves two in place and the same message reaches your handler twice.
  • History is not replayed on a reconnect, since your app still holds those messages. Anything that arrived while you were offline is reported through onMessageDrop instead.
  • History is replayed after a room switch, since that is a different conversation.
  • If restoration fails you get PUBSUB_SUBSCRIBE_FAILED (4088) on Events.error, and on the event only — there is no call left to throw from.

7. PubSubMessage.sendOnly

PubSubMessage gained a sendOnly field carrying the participant IDs a message was addressed to, when it was published with PubSubPublishOptions.sendOnly. It is an empty list for a broadcast message.

void messageHandler(PubSubMessage message) {
if (message.sendOnly.isNotEmpty) {
// A private message, addressed to these participant IDs.
log("Private message for ${message.sendOnly}");
}
}

This is additive — the field was previously available only on the publishing side, so a receiver had no way to tell a direct message from a broadcast.


8. Breaking: two corrections to the error catalog

Two long-standing mistakes in the error catalog are fixed. Both are visible to your app, so check these if you match on an error's name or code inside your Events.error handler.

What changedBeforeNow
Name of the participant-limit error 4009MAX_PARTCIPANT_REACHEDMAX_PARTICIPANT_REACHED
Code carried by the speaker-limit error 4010"4009""4010"

Before

room.on(Events.error, (error) {
if (error['name'] == "MAX_PARTCIPANT_REACHED") {
showRoomFullDialog();
}
});

After

room.on(Events.error, (error) {
if (error['name'] == "MAX_PARTICIPANT_REACHED") {
showRoomFullDialog();
}
});

Key points:

  • The first is a spelling fix — PARTCIPANT was missing its second I. Any string comparison against the old spelling stops matching.
  • The second means the speaker-limit error now reports its own code. Code 4009 previously arrived for both, so a handler keyed on error['code'] == "4009" was firing for 4010 too and will now stop.
  • Learn more: Meeting Error Codes

Got a Question? Ask us on discord