Datum Singleton API

📖 10 min read

The Datum class provides a global singleton instance that offers convenient access to all Datum functionality. While you can access managers directly through Datum.manager<T>(), the singleton also provides high-level convenience methods for common operations.

Initialization#

Before using any Datum functionality, you must initialize the singleton:

final result = await Datum.initialize(
  config: DatumConfig(
    enableLogging: true,
    autoStartSync: true,
    autoSyncInterval: Duration(minutes: 5),
  ),
  connectivityChecker: MyConnectivityChecker(),
  registrations: [
    DatumRegistration<Task>(
      localAdapter: HiveTaskAdapter(),
      remoteAdapter: RestApiTaskAdapter(),
    ),
  ],
);

if (result.isSuccess) {
  // Datum is ready to use
} else {
  // Handle initialization error
  print('Failed to initialize Datum: ${result.error}');
}

Accessing Managers#

Get a manager for a specific entity type:

final taskManager = Datum.manager<Task>();
**Tip**: The singleton methods are perfect for simple operations. For advanced features like custom conflict resolution or detailed event monitoring, use the manager APIs directly.

Convenience CRUD Methods#

The singleton provides direct access to CRUD operations without needing to get managers first:

Create Operations#

// Create a single entity
final task = Task(id: '1', title: 'New Task', userId: 'user123');
await Datum.create(task);

// Create multiple entities
final tasks = [
  Task(id: '1', title: 'Task 1', userId: 'user123'),
  Task(id: '2', title: 'Task 2', userId: 'user123'),
];
await Datum.createMany<Task>(items: tasks, userId: 'user123');

Read Operations#

// Read a single entity
final task = await Datum.read<Task>('task-id', userId: 'user123');

// Read all entities for a user
final allTasks = await Datum.readAll<Task>(userId: 'user123');

// Query entities
final query = DatumQueryBuilder<Task>()
  .where('completed', isEqualTo: false)
  .orderBy('createdAt', descending: true)
  .build();

final pendingTasks = await Datum.query<Task>(
  query,
  source: DataSource.local,
  userId: 'user123',
);

Update Operations#

// Update a single entity
final updatedTask = existingTask.copyWith(title: 'Updated Title');
await Datum.update(updatedTask);

// Update multiple entities
final tasksToUpdate = [task1, task2, task3];
await Datum.updateMany<Task>(items: tasksToUpdate, userId: 'user123');

Delete Operations#

// Delete a single entity
await Datum.delete<Task>(id: 'task-id', userId: 'user123');

Sync Operations#

Immediate Sync#

// Create/update and immediately sync
final (savedTask, syncResult) = await Datum.pushAndSync(
  item: task,
  userId: 'user123',
);

// Update and immediately sync
final (updatedTask, syncResult) = await Datum.updateAndSync(
  item: task,
  userId: 'user123',
);

// Delete and immediately sync
final (deleted, syncResult) = await Datum.deleteAndSync<Task>(
  id: 'task-id',
  userId: 'user123',
);

Global Sync#

Synchronize all registered entity types for a user:

final syncResult = await Datum.instance.synchronize('user123');
print('Synced ${syncResult.syncedCount} items across all entities');

User Change Streams#

Datum provides reactive user change streams that automatically update data when users switch. This is particularly useful for multi-tenant applications where different users have separate data.

Using Datum.userChangeStream#

Listen to global user changes across all entity types:

// Listen to global user changes
final subscription = Datum.instance.userChangeStream.listen((userId) {
  print('User changed to: $userId');
  // Automatically refresh UI or data
});

// Emit user changes when authentication state changes
Datum.instance._userChangeController.add('new-user-id');

Using Manager.onUserChanged#

Listen to user changes for specific entity types:

// Get a specific manager
final taskManager = Datum.manager<Task>();

// Listen to user changes for this entity type
final userSubscription = taskManager.onUserChanged.listen((userId) {
  print('User changed for tasks: $userId');
  // Tasks will automatically refresh for the new user
});

Integration with Authentication#

class AuthService {
  void login(String userId) {
    // Update Datum's user change stream
    Datum.instance._userChangeController.add(userId);

    // Your authentication logic here
    // ...
  }

  void logout() {
    // Clear user (null indicates no user)
    Datum.instance._userChangeController.add(null);
  }

  void switchUser(String newUserId) {
    // Switch to different user
    Datum.instance._userChangeController.add(newUserId);
  }
}

Reactive UI Updates#

class TaskListWidget extends StatefulWidget {
  @override
  _TaskListWidgetState createState() => _TaskListWidgetState();
}

class _TaskListWidgetState extends State<TaskListWidget> {
  late StreamSubscription<String?> _userSubscription;
  List<Task> _tasks = [];

  @override
  void initState() {
    super.initState();

    // Listen to user changes and refresh data
    _userSubscription = Datum.manager<Task>().onUserChanged.listen((userId) {
      _loadTasks();
    });

    _loadTasks(); // Initial load
  }

  Future<void> _loadTasks() async {
    final tasks = await Datum.manager<Task>().readAll();
    setState(() => _tasks = tasks);
  }

  @override
  void dispose() {
    _userSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _tasks.length,
      itemBuilder: (context, index) {
        return ListTile(title: Text(_tasks[index].title));
      },
    );
  }
}

Benefits#

  • Automatic Data Isolation: Each user's data is kept separate
  • Reactive Updates: UI automatically refreshes when users switch
  • Memory Management: Old user's data is cleaned up automatically
  • Performance: Only active user's data is loaded in memory
  • Security: Prevents data leakage between users

Stream Management#

Refreshing Streams#

Datum provides a refreshStreams() method to force all reactive streams to re-evaluate their data. This is particularly useful when external state changes require all streams to refresh their data.

// Refresh all streams across all managers
await Datum.instance.refreshStreams();

// This will:
// - Clear internal caches in all managers
// - Force reactive streams to emit fresh data
// - Ensure streams show the most current data after state changes

Use Cases#

  • User Switching: When switching between users, refresh streams to show the new user's data
  • External Data Changes: When external systems modify data that Datum isn't aware of
  • Cache Invalidation: When you need to ensure all streams have the latest data
  • Testing: In test scenarios where you need to reset stream state

Manager-Level Refresh#

You can also refresh streams for specific entity types:

// Refresh streams for a specific entity type
final taskManager = Datum.manager<Task>();
await taskManager.refreshStreams();

Automatic Refresh#

Streams are automatically refreshed in certain scenarios:

  • When users switch (via onUserChanged streams)
  • After certain sync operations
  • When the system detects state inconsistencies

Performance Considerations#

  • refreshStreams() clears all internal caches, which may impact performance
  • Use sparingly and only when necessary
  • Consider using targeted cache invalidation for better performance when possible

Combining onUserChanged and refreshStreams#

For optimal user switching behavior, combine onUserChanged with refreshStreams:

class UserManager {
  StreamSubscription<String?>? _userSubscription;

  void initialize() {
    // Listen to user changes and refresh streams
    _userSubscription = Datum.manager<Task>().onUserChanged.listen((userId) {
      print('User changed to: $userId');

      // Refresh streams to clear user-specific caches
      Datum.instance.refreshStreams();

      // Additional user switch logic
      _onUserSwitched(userId);
    });
  }

  void _onUserSwitched(String? userId) {
    if (userId == null) {
      // User logged out - clear any user-specific state
      _clearUserState();
    } else {
      // User logged in - load user preferences, update UI, etc.
      _loadUserPreferences(userId);
      _updateUIForUser(userId);
    }
  }

  void dispose() {
    _userSubscription?.cancel();
  }
}

Advanced User Switching Pattern#

class AdvancedUserSwitcher {
  final StreamController<String?> _userController = StreamController.broadcast();

  // Expose user change stream for other components
  Stream<String?> get onUserChanged => _userController.stream;

  Future<void> switchToUser(String newUserId) async {
    final currentUserId = await _getCurrentUserId();

    // 1. Validate user switch (check permissions, etc.)
    if (!await _canSwitchToUser(newUserId)) {
      throw Exception('Cannot switch to user: insufficient permissions');
    }

    // 2. Pre-switch cleanup
    if (currentUserId != null) {
      await _cleanupUserData(currentUserId);
    }

    // 3. Perform the switch
    await _performUserSwitch(currentUserId, newUserId);

    // 4. Emit user change event
    _userController.add(newUserId);

    // 5. Refresh all streams to clear caches
    await Datum.instance.refreshStreams();

    // 6. Post-switch initialization
    await _initializeUserData(newUserId);
  }

  Future<void> logout() async {
    final currentUserId = await _getCurrentUserId();
    if (currentUserId != null) {
      await _cleanupUserData(currentUserId);
    }

    // Clear user and refresh streams
    _userController.add(null);
    await Datum.instance.refreshStreams();

    // Clear authentication state
    await _clearAuthState();
  }
}

Reactive UI with User Switching#

class MultiUserApp extends StatefulWidget {
  @override
  _MultiUserAppState createState() => _MultiUserAppState();
}

class _MultiUserAppState extends State<MultiUserApp> {
  late StreamSubscription<String?> _userSubscription;
  String? _currentUserId;
  List<Task> _tasks = [];

  @override
  void initState() {
    super.initState();

    // Listen to user changes
    _userSubscription = Datum.manager<Task>().onUserChanged.listen((userId) {
      setState(() => _currentUserId = userId);

      if (userId != null) {
        // Load user's tasks when they switch in
        _loadUserTasks(userId);
      } else {
        // Clear tasks when user logs out
        setState(() => _tasks = []);
      }
    });

    // Initial load
    _loadCurrentUser();
  }

  Future<void> _loadUserTasks(String userId) async {
    final tasks = await Datum.manager<Task>().readAll(userId: userId);
    if (mounted) {
      setState(() => _tasks = tasks);
    }
  }

  Future<void> _loadCurrentUser() async {
    // Get current user from your auth system
    final userId = await _getCurrentUserId();
    setState(() => _currentUserId = userId);

    if (userId != null) {
      await _loadUserTasks(userId);
    }
  }

  @override
  void dispose() {
    _userSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_currentUserId != null
          ? 'Tasks for User $_currentUserId'
          : 'Please log in'),
      ),
      body: _currentUserId == null
        ? Center(child: Text('No user logged in'))
        : TaskList(tasks: _tasks, userId: _currentUserId!),
    );
  }
}

Benefits of Combined Usage#

  • Automatic Cache Management: refreshStreams() ensures no stale data from previous user
  • Reactive UI Updates: onUserChanged triggers immediate UI updates
  • Data Isolation: Each user's data is properly separated and cached
  • Performance: Targeted cache clearing prevents memory leaks
  • Consistency: All reactive streams show correct data for current user

Reactive Operations#

Watch for real-time data changes:

// Watch all entities
final subscription = Datum.watchAll<Task>(userId: 'user123')
  ?.listen((tasks) {
    print('Tasks updated: ${tasks.length} items');
  });

// Watch a single entity
final singleSub = Datum.watchById<Task>('task-id', 'user123')
  ?.listen((task) {
    if (task != null) {
      print('Task updated: ${task.title}');
    } else {
      print('Task was deleted');
    }
  });

// Watch paginated results
final paginatedSub = Datum.watchAllPaginated<Task>(
  PaginationConfig(pageSize: 20),
  userId: 'user123',
)?.listen((result) {
  print('Page ${result.currentPage}: ${result.items.length} items');
});

// Watch query results
final query = DatumQueryBuilder<Task>()
  .where('completed', isEqualTo: false)
  .build();

final querySub = Datum.watchQuery<Task>(query, userId: 'user123')
  ?.listen((tasks) {
    print('Pending tasks: ${tasks.length}');
  });

Relationship Operations#

Work with related entities:

// Fetch related entities
final comments = await Datum.fetchRelated<Post, Comment>(
  post,
  'comments',
  source: DataSource.local,
);

// Watch related entities
final relatedSub = Datum.watchRelated<Post, Comment>(post, 'comments')
  ?.listen((comments) {
    print('Post has ${comments.length} comments');
  });

Monitoring & Health#

Health Monitoring#

// Check health of all managers
Datum.instance.allHealths.listen((healthMap) {
  healthMap.forEach((entityType, health) {
    print('$entityType: ${health.status}');
  });
});

// Check health of specific entity type
final health = await Datum.checkHealth<Task>();
print('Task health: ${health.status}');

Metrics#

// Monitor global metrics
Datum.instance.metrics.listen((metrics) {
  print('Total syncs: ${metrics.totalSyncOperations}');
  print('Successful: ${metrics.successfulSyncs}');
  print('Failed: ${metrics.failedSyncs}');
});

User Status#

// Monitor sync status for a user
Datum.instance.statusForUser('user123').listen((status) {
  if (status != null) {
    print('User sync status: ${status.status}');
    print('Pending operations: ${status.pendingOperationsCount}');
  }
});

Utility Methods#

Pending Operations#

// Get pending operation count
final count = await Datum.getPendingCount<Task>('user123');

// Get pending operations
final operations = await Datum.getPendingOperations<Task>('user123');

Storage Information#

// Get storage size
final size = await Datum.getStorageSize<Task>(userId: 'user123');

// Watch storage size changes
Datum.watchStorageSize<Task>(userId: 'user123')?.listen((size) {
  print('Storage size: ${size} bytes');
});

Sync Results#

// Get last sync result
final lastResult = await Datum.getLastSyncResult<Task>('user123');

// Get remote sync metadata
final metadata = await Datum.getRemoteSyncMetadata<Task>('user123');

Global Sync Control#

Control synchronization across all managers:

// Pause all sync operations
Datum.instance.pauseSync();

// Resume all sync operations
Datum.instance.resumeSync();

// Unsubscribe from remote changes
await Datum.instance.unsubscribeAllFromRemoteChanges();

// Resubscribe to remote changes
await Datum.instance.resubscribeAllToRemoteChanges();

Best Practices#

  1. Initialization: Always initialize Datum before use
  2. Error Handling: Check initialization results and handle sync errors
  3. Resource Management: Cancel subscriptions when no longer needed
  4. Performance: Use appropriate data sources (local vs remote) for your use case
  5. Monitoring: Monitor health and metrics in production applications

Comparison with Manager API#

OperationSingleton MethodManager Method
Create Datum.create(entity) manager.push(item: entity)
ReadDatum.read<T>(id)manager.read(id)
Watch Datum.watchAll<T>() manager.watchAll()
Sync Datum.instance.synchronize() manager.synchronize()

The singleton methods are convenient for simple operations, while manager methods provide more control and advanced features.

Examples#

// Initialize Datum
await Datum.initialize(
  config: DatumConfig(),
  connectivityChecker: MyConnectivityChecker(),
  registrations: [DatumRegistration<Task>(/* adapters */)],
);

// Use the singleton API
final task = Task(id: '1', title: 'My Task', userId: 'user123');
await Datum.create(task);

final tasks = await Datum.readAll<Task>(userId: 'user123');
print('Found ${tasks.length} tasks');

Status Indicators#

Current API Status: Stable

Available in: v1.0.2+

Component Showcase#

Here are some examples of the enhanced documentation components:

  1. Initialize Datum with your configuration and adapters
  2. Use the singleton API for convenient operations
  3. Monitor health and performance metrics
  4. Handle sync conflicts with built-in resolvers
Advanced Features
The singleton API provides powerful features beyond basic CRUD operations:
  • Real-time watching with reactive streams
  • Batch operations for multiple entities
  • Relationship queries with eager loading
  • Global sync control across all managers
  • Health monitoring and metrics collection

These features make it easy to build sophisticated offline-first applications with minimal boilerplate code.

**Performance Note**: While the singleton API is convenient, for high-frequency operations in performance-critical code paths, consider using manager instances directly to avoid the additional indirection.