Flutter InstantDB
API Reference

Presence API

Complete API reference for InstantDB presence and real-time collaboration

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:

On this page