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>();
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
onUserChangedstreams) - 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:
onUserChangedtriggers 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#
- Initialization: Always initialize Datum before use
- Error Handling: Check initialization results and handle sync errors
- Resource Management: Cancel subscriptions when no longer needed
- Performance: Use appropriate data sources (local vs remote) for your use case
- Monitoring: Monitor health and metrics in production applications
Comparison with Manager API#
| Operation | Singleton Method | Manager Method |
|---|---|---|
| Create | Datum.create(entity) |
manager.push(item: entity) |
| Read | Datum.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:
Available in:
Component Showcase#
Here are some examples of the enhanced documentation components:
-
Initialize Datum with your configuration and adapters
-
Use the singleton API for convenient operations
-
Monitor health and performance metrics
-
Handle sync conflicts with built-in resolvers
- 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.