Skip to Content
API ReferencePresence API

The Presence API enables real-time collaboration features like cursors, typing indicators, reactions, and user awareness. It provides both room-based and direct APIs for building collaborative applications.

PresenceManager

The main entry point for presence functionality, accessed via db.presence.

joinRoom()

Join a presence room and get a scoped API for room operations.

InstantRoom joinRoom( String roomId, { Map<String, dynamic>? initialPresence, })

Parameters:

  • roomId (String): Unique identifier for the room
  • initialPresence (Map<String, dynamic>?): Initial presence data

Returns: InstantRoom - Room instance with scoped operations

Example:

final room = db.presence.joinRoom('editor-room', initialPresence: { 'userName': 'Alice', 'status': 'online', 'color': '#ff6b6b', });

leaveRoom()

Leave a presence room and clean up resources.

Future<void> leaveRoom(String roomId)

Parameters:

  • roomId (String): Room ID to leave

Example:

await db.presence.leaveRoom('editor-room');

Direct Presence Methods

For simple use cases, you can use direct methods without joining a room:

setPresence()

Future<void> setPresence(String roomId, Map<String, dynamic> presence)

updateCursor()

Future<void> updateCursor(String roomId, {required double x, required double y})

setTyping()

Future<void> setTyping(String roomId, bool isTyping)

sendReaction()

Future<void> sendReaction(String roomId, String reaction, {Map<String, dynamic>? metadata})

InstantRoom

Room-scoped presence API providing isolated operations for a specific collaboration space.

Presence Operations

setPresence()

Update user presence data in the room.

Future<void> setPresence(Map<String, dynamic> presence)

Parameters:

  • presence (Map<String, dynamic>): Presence data to set

Example:

await room.setPresence({ 'status': 'editing', 'currentDocument': documentId, 'mood': '😊', 'tool': 'text-editor', });

getPresence()

Get a reactive signal of all user presence data in the room.

Signal<Map<String, PresenceData>> getPresence()

Returns: Signal<Map<String, PresenceData>> - Map of user ID to presence data

Example:

Watch((context) { final presence = room.getPresence().value; final onlineCount = presence.values.length; return Text('$onlineCount users in room'); });

Cursor Operations

updateCursor()

Update cursor position in the room.

Future<void> updateCursor({required double x, required double y})

Parameters:

  • x (double): X coordinate
  • y (double): Y coordinate

Example:

void _onMouseMove(PointerEvent event) { room.updateCursor( x: event.localPosition.dx, y: event.localPosition.dy, ); }

getCursors()

Get a reactive signal of all cursor positions in the room.

Signal<Map<String, CursorData>> getCursors()

Returns: Signal<Map<String, CursorData>> - Map of user ID to cursor data

Example:

Watch((context) { final cursors = room.getCursors().value; return Stack( children: cursors.entries.map((entry) { final cursor = entry.value; return Positioned( left: cursor.x, top: cursor.y, child: CursorWidget( userName: cursor.userName ?? 'Unknown', userColor: cursor.userColor != null ? Color(int.parse(cursor.userColor!.replaceAll('#', '0xFF'))) : Colors.blue, ), ); }).toList(), ); });

Typing Operations

setTyping()

Set typing indicator status.

Future<void> setTyping(bool isTyping)

Parameters:

  • isTyping (bool): Whether user is currently typing

Example:

final _textController = TextEditingController(); Timer? _typingTimer; void _onTextChanged(String text) { // Set typing to true room.setTyping(true); // Clear typing after delay _typingTimer?.cancel(); _typingTimer = Timer(const Duration(seconds: 2), () { room.setTyping(false); }); }

getTyping()

Get a reactive signal of users currently typing. Returns a map of user IDs to the timestamp they were last seen typing.

Signal<Map<String, DateTime>> getTyping()

Returns: Signal<Map<String, DateTime>> - Map of user IDs to typing timestamps

Example:

Watch((context) { final typing = room.getTyping().value; if (typing.isEmpty) { return const SizedBox.shrink(); } return Text('${typing.length} people are typing...'); });

Reaction Operations

sendReaction()

Send a reaction to the room.

Future<void> sendReaction( String reaction, { Map<String, dynamic>? metadata, })

Parameters:

  • reaction (String): Reaction emoji or identifier
  • metadata (Map<String, dynamic>?): Additional reaction data

Example:

void _onDoubleTap(TapDownDetails details) { room.sendReaction('❤️', metadata: { 'x': details.localPosition.dx, 'y': details.localPosition.dy, 'message': 'Great idea!', 'timestamp': DateTime.now().millisecondsSinceEpoch, }); }

getReactions()

Get a reactive signal of recent reactions.

Signal<List<ReactionData>> getReactions()

Returns: Signal<List<ReactionData>> - List of recent reactions

Example:

Watch((context) { final reactions = room.getReactions().value; return Stack( children: reactions.map((reaction) { final metadata = reaction.metadata; final x = metadata?['x']?.toDouble() ?? 0.0; final y = metadata?['y']?.toDouble() ?? 0.0; return Positioned( left: x, top: y, child: AnimatedReaction( emoji: reaction.emoji, onComplete: () { // Reaction animation completed }, ), ); }).toList(), ); });

Topic-Based Messaging

Rooms support topic-based messaging for structured communication.

publishTopic()

Publish a message to a specific topic within the room.

Future<void> publishTopic(String topic, Map<String, dynamic> data)

Parameters:

  • topic (String): Topic name
  • data (Map<String, dynamic>): Message data

Example:

await room.publishTopic('chat', { 'message': 'Hello everyone!', 'userName': 'Alice', 'timestamp': DateTime.now().millisecondsSinceEpoch, }); await room.publishTopic('document-changes', { 'type': 'text-insert', 'position': 42, 'text': 'Hello', 'userId': currentUserId, });

subscribeTopic()

Subscribe to messages on a specific topic.

Stream<Map<String, dynamic>> subscribeTopic(String topic)

Parameters:

  • topic (String): Topic name to subscribe to

Returns: Stream<Map<String, dynamic>> - Stream of messages

Example:

class ChatRoomWidget extends StatefulWidget { final InstantRoom room; const ChatRoomWidget({super.key, required this.room}); @override State<ChatRoomWidget> createState() => _ChatRoomWidgetState(); } class _ChatRoomWidgetState extends State<ChatRoomWidget> { final List<ChatMessage> _messages = []; StreamSubscription? _chatSubscription; @override void initState() { super.initState(); // Subscribe to chat messages _chatSubscription = widget.room.subscribeTopic('chat').listen((data) { final message = ChatMessage.fromJson(data); setState(() { _messages.add(message); }); }); } @override void dispose() { _chatSubscription?.cancel(); super.dispose(); } void _sendMessage(String text) { widget.room.publishTopic('chat', { 'message': text, 'userName': 'Current User', 'timestamp': DateTime.now().millisecondsSinceEpoch, }); } @override Widget build(BuildContext context) { return Column( children: [ Expanded( child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { return MessageBubble(message: _messages[index]); }, ), ), MessageInput(onSend: _sendMessage), ], ); } }

Data Types

PresenceData

Represents a user’s presence data.

class PresenceData { final String userId; final Map<String, dynamic> data; final DateTime lastSeen; const PresenceData({ required this.userId, required this.data, required this.lastSeen, }); }

Properties:

  • userId (String): Unique user identifier
  • data (Map<String, dynamic>): Custom presence data
  • lastSeen (DateTime): When user was last seen in the room

CursorData

Represents cursor position and metadata.

class CursorData { final String userId; final String? userName; final String? userColor; final double x; final double y; final Map<String, dynamic>? metadata; final DateTime lastUpdated; const CursorData({ required this.userId, this.userName, this.userColor, required this.x, required this.y, this.metadata, required this.lastUpdated, }); }

Properties:

  • userId (String): User who owns the cursor
  • userName (String?): Optional display name for the user
  • userColor (String?): Optional hex color string for the cursor
  • x (double): X coordinate
  • y (double): Y coordinate
  • metadata (Map<String, dynamic>?): Additional custom cursor metadata
  • lastUpdated (DateTime): When cursor was last updated

ReactionData

Represents a reaction sent to the room.

class ReactionData { final String id; final String userId; final String emoji; final String? messageId; final Map<String, dynamic>? metadata; final DateTime timestamp; const ReactionData({ required this.id, required this.userId, required this.emoji, this.messageId, this.metadata, required this.timestamp, }); }

Properties:

  • id (String): Unique reaction identifier
  • userId (String): User who sent the reaction
  • emoji (String): Reaction emoji
  • messageId (String?): Optional ID of a message this reaction refers to
  • metadata (Map<String, dynamic>?): Custom reaction metadata
  • timestamp (DateTime): When reaction was created

Complete Examples

Collaborative Text Editor

class CollaborativeEditor extends StatefulWidget { final String documentId; const CollaborativeEditor({super.key, required this.documentId}); @override State<CollaborativeEditor> createState() => _CollaborativeEditorState(); } class _CollaborativeEditorState extends State<CollaborativeEditor> { final TextEditingController _controller = TextEditingController(); InstantRoom? _room; Timer? _typingTimer; @override void initState() { super.initState(); _initializeCollaboration(); } void _initializeCollaboration() { final db = InstantProvider.of(context); final currentUser = db.auth.currentUser.value; _room = db.presence.joinRoom('doc-${widget.documentId}', initialPresence: { 'userName': currentUser?.email ?? 'Anonymous', 'status': 'editing', 'color': _generateUserColor(currentUser?.id ?? 'anonymous'), }); // Listen to text changes for typing indicators _controller.addListener(_handleTextChange); // Listen to selection changes for cursor updates _controller.addListener(_handleSelectionChange); } void _handleTextChange() { // Set typing status _room?.setTyping(true); // Clear typing after delay _typingTimer?.cancel(); _typingTimer = Timer(const Duration(seconds: 2), () { _room?.setTyping(false); }); } void _handleSelectionChange() { final selection = _controller.selection; if (selection.isValid) { // Convert text position to screen coordinates (simplified) final cursorX = selection.baseOffset * 10.0; // Approximation final cursorY = 0.0; _room?.updateCursor(x: cursorX, y: cursorY); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Collaborative Editor'), actions: [ // Show connected users if (_room != null) UserAvatars(room: _room!), ], ), body: Column( children: [ // Typing indicators if (_room != null) TypingIndicator(room: _room!), // Main editor Expanded( child: Stack( children: [ // Text input Padding( padding: const EdgeInsets.all(16), child: TextField( controller: _controller, maxLines: null, expands: true, decoration: const InputDecoration( border: InputBorder.none, hintText: 'Start typing...', ), ), ), // Cursor overlay if (_room != null) CursorOverlay(room: _room!), // Reactions overlay if (_room != null) ReactionsOverlay(room: _room!), ], ), ), ], ), ); } Color _generateUserColor(String userId) { final hash = userId.hashCode; return Color(0xFF000000 | (hash & 0xFFFFFF)); } @override void dispose() { _room?.setPresence({'status': 'offline'}); _controller.dispose(); _typingTimer?.cancel(); super.dispose(); } }

Multiplayer Whiteboard

class CollaborativeWhiteboard extends StatefulWidget { @override State<CollaborativeWhiteboard> createState() => _CollaborativeWhiteboardState(); } class _CollaborativeWhiteboardState extends State<CollaborativeWhiteboard> { InstantRoom? _room; final List<DrawingPoint> _points = []; StreamSubscription? _drawingSubscription; @override void initState() { super.initState(); _initializeWhiteboard(); } void _initializeWhiteboard() { final db = InstantProvider.of(context); _room = db.presence.joinRoom('whiteboard', initialPresence: { 'userName': 'Artist ${DateTime.now().millisecondsSinceEpoch % 1000}', 'tool': 'pen', 'color': '#000000', }); // Subscribe to drawing events _drawingSubscription = _room!.subscribeTopic('drawing').listen((data) { final point = DrawingPoint.fromJson(data); setState(() { _points.add(point); }); }); } void _handlePanStart(DragStartDetails details) { _room?.setPresence({'status': 'drawing'}); _addPoint(details.localPosition, isStart: true); } void _handlePanUpdate(DragUpdateDetails details) { _addPoint(details.localPosition, isStart: false); // Update cursor for other users _room?.updateCursor( x: details.localPosition.dx, y: details.localPosition.dy, ); } void _handlePanEnd(DragEndDetails details) { _room?.setPresence({'status': 'idle'}); } void _addPoint(Offset position, {required bool isStart}) { final point = DrawingPoint( x: position.dx, y: position.dy, isStart: isStart, userId: 'current-user', // Get from auth timestamp: DateTime.now().millisecondsSinceEpoch, ); // Broadcast drawing point _room?.publishTopic('drawing', point.toJson()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Collaborative Whiteboard'), actions: [ if (_room != null) UserAvatars(room: _room!), ], ), body: Stack( children: [ // Drawing canvas GestureDetector( onPanStart: _handlePanStart, onPanUpdate: _handlePanUpdate, onPanEnd: _handlePanEnd, child: CustomPaint( painter: WhiteboardPainter(_points), size: Size.infinite, ), ), // Collaborative cursors if (_room != null) CursorOverlay(room: _room!), // Reactions if (_room != null) GestureDetector( onDoubleTapDown: (details) { _room!.sendReaction('✨', metadata: { 'x': details.localPosition.dx, 'y': details.localPosition.dy, }); }, child: ReactionsOverlay(room: _room!), ), ], ), ); } @override void dispose() { _room?.setPresence({'status': 'offline'}); _drawingSubscription?.cancel(); super.dispose(); } }

Performance Considerations

Throttle Updates

Prevent excessive presence updates:

class ThrottledPresence { final InstantRoom room; Timer? _cursorTimer; static const Duration _throttleDuration = Duration(milliseconds: 100); ThrottledPresence(this.room); void updateCursor(double x, double y) { _cursorTimer?.cancel(); _cursorTimer = Timer(_throttleDuration, () { room.updateCursor(x: x, y: y); }); } void dispose() { _cursorTimer?.cancel(); } }

Limit Displayed Elements

Prevent performance issues with many users:

Watch((context) { final cursors = room.getCursors().value; // Limit to 10 most recent cursors final recentCursors = cursors.entries .toList() ..sort((a, b) => b.value.lastUpdated.compareTo(a.value.lastUpdated)) ..take(10); return Stack( children: recentCursors.map((entry) => CursorWidget(cursor: entry.value) ).toList(), ); });

Error Handling

class SafePresenceOperations { final InstantRoom room; SafePresenceOperations(this.room); Future<void> safeSetPresence(Map<String, dynamic> presence) async { try { await room.setPresence(presence); } catch (e) { print('Failed to set presence: $e'); // Handle gracefully - presence is not critical } } Future<void> safeUpdateCursor(double x, double y) async { try { await room.updateCursor(x: x, y: y); } catch (e) { print('Failed to update cursor: $e'); // Cursor updates are not critical } } }

Next Steps

Explore related APIs and features: