utd_gift_kit 0.1.0
utd_gift_kit: ^0.1.0 copied to clipboard
In-room virtual gifts for Flutter: a typed gift protocol and a play queue that serialises overlapping gifts. No plugin dependencies. Transport-agnostic — works in an audio room, a live stream, or any [...]
utd_gift_kit #
In-room virtual gifts for Flutter: a typed gift protocol and a play queue that stops gifts animating on top of each other.
No plugin dependencies — adding it costs your app nothing. Transport-agnostic: the same code serves an audio room, a live stream, or your own channel.
What this does and does not do #
| Gift catalogue, prices, animation files, caching them | your app and your server |
| Balance, ledger, the charge itself | your backend |
| Carrying the gift event and ordering it | this package |
It ships no animation player and no file cache on purpose: every app already has both, and a second copy of either is dead weight on the user's device. The genuinely hard part — the part every integrator ends up rewriting — is the ordering and the lifecycle, and that is what this owns.
No money passes through here. sendGift broadcasts a display event. A
client-side send is spoofable by construction, so it must never be what a
charge is based on: commit the transaction on your server, and either call
sendGift after it succeeds or — better — have your server emit the same
message (see Server-authoritative gifts below).
Install #
dependencies:
utd_gift_kit: ^0.1.0
Wire it to a room #
The package binds to anything that can send and receive maps, so both room kits work with the same three lines:
import 'package:utd_gift_kit/utd_gift_kit.dart';
final gifts = UTDGiftManager.instance;
gifts.attach(UTDGiftTransport.from(
send: (data, {to}) => controller.sendRoomMessage(data),
incoming: controller.dataStream,
localIdentity: () => controller.localIdentity,
));
// Your catalogue turns a gift id into a file — this package has no catalogue.
gifts.assetResolver = (event) =>
UTDGiftAsset(url: myCatalogue[event.giftId]!.animationUrl);
Call gifts.detach() when leaving the room. Attaching to a new room clears the
queue automatically: gifts belong to the room they were sent in.
Send #
await gifts.sendGift(
giftId: 'rose',
count: 10, // a x10 combo is ONE event, not ten
receiverIds: ['user-42'], // omit for the whole room
extra: {'pk_round': 3}, // your data, passed through untouched
);
Show it #
Rendering is yours: every app already has the animation player it wants, and forcing a second one on every consumer would cost more than it gives. This package owns the hard part — the ordering and the lifecycle.
UTDGiftOverlay(
playList: gifts.playList,
builder: (context, data, onDone) => MyGiftAnimation(
url: data.asset?.url, // your cache/CDN resolves this
onComplete: onDone, // call on completion AND on failure
),
)
onDoneadvances the queue. Call it when the animation fails too, or the next gift waits behind one that will never finish.UTDGiftPlayListhas amaxPlayDurationbackstop (15s) for exactly that case, but it is a safety net, not a substitute.
Server-authoritative gifts (recommended when money is involved) #
UTDGiftProtocol.encode is public so your backend can produce the exact
message clients decode. Charge the sender, then have the server broadcast:
// Shape your server sends (via the engine's send-data API):
{
"type": "_utd_gift",
"gift_id": "rose",
"count": 10,
"sender_id": "user-1",
"sender_name": "Ali",
"receiver_ids": ["user-42"],
"ts": "2026-09-04T10:00:00.000Z",
"extra": {"pk_round": 3}
}
Clients need no change — giftStream delivers it identically.
Notes #
- A malformed gift from any peer is dropped, never thrown: the same channel carries seat updates and chat, and one bad frame must not break them.
- Your own gift is emitted locally once and the room's echo of it is ignored, so it never animates twice.
- The queue is bounded (
maxQueueLength, default 100): a gift storm drops the oldest pending rather than growing memory, so the room keeps showing what is happening now.