Adapter Module#
The Adapter module provides the abstraction layer that allows Datum to work with any local database and remote backend. This module defines the interfaces and base implementations for adapters.
Local Adapters#
Local adapters handle data persistence on the device. Datum provides built-in adapters for popular databases and a framework for creating custom ones.
Built-in Local Adapters#
HiveAdapter
A high-performance, lightweight key-value database adapter for Hive.
Features:
- Fast read/write operations
- Type-safe storage with generated type adapters
- Automatic JSON serialization
- Box management per user/entity
Usage:
class TaskAdapter extends HiveAdapter<Task> {
TaskAdapter() : super(
boxName: 'tasks',
fromJson: Task.fromJson,
toJson: (task) => task.toJson(),
);
}
IsarAdapter
An adapter for the Isar database, offering advanced querying and relationships.
Features:
- Advanced querying capabilities
- Built-in indexing support
- Multi-platform support
- Schema migration support
Usage:
class TaskAdapter extends IsarAdapter<Task> {
TaskAdapter(Isar isar) : super(
isar: isar,
collectionName: 'Task',
fromJson: Task.fromJson,
toJson: (task) => task.toJson(),
);
}
Custom Local Adapters#
Implement LocalAdapter<T> to create adapters for other databases.
Required Methods:
initialize(): Set up the adaptercreate(T entity): Insert new entityread(String id, {String? userId}): Retrieve entity by IDreadAll({String? userId}): Retrieve all entitiespatch(String id, Map<String, dynamic> delta, {String? userId}): Update entitydelete(String id, {String? userId}): Delete entityquery(DatumQuery query, {String? userId}): Execute queriesgetAllUserIds(): Get all user IDs with dataclearUserData(String userId): Remove all data for a userchangeStream(): Stream of data changeswatchAll({String? userId, bool includeInitialData}): Reactive all entitieswatchById(String id, {String? userId}): Reactive single entitywatchQuery(DatumQuery query, {String? userId}): Reactive queriesgetStorageSize({String? userId}): Get storage sizewatchStorageSize({String? userId}): Watch storage size changes
Remote Adapters#
Remote adapters handle communication with backend services and APIs.
Built-in Remote Adapters#
RestApiAdapter
A flexible REST API adapter for HTTP-based backends.
Features:
- Configurable endpoints and HTTP methods
- Automatic retry logic
- Custom headers and authentication
- JSON serialization
Usage:
class TaskApiAdapter extends RestApiAdapter<Task> {
TaskApiAdapter(Dio dio) : super(
dio: dio,
baseUrl: 'https://api.example.com',
endpoints: ApiEndpoints(
create: '/tasks',
read: '/tasks/:id',
readAll: '/tasks',
update: '/tasks/:id',
delete: '/tasks/:id',
),
fromJson: Task.fromJson,
toJson: (task) => task.toJson(),
);
}
SupabaseAdapter
Specialized adapter for Supabase backend services.
Features:
- Real-time subscriptions
- Built-in authentication
- Row Level Security (RLS) support
- Automatic conflict detection
FirebaseAdapter
Adapter for Firebase Firestore with offline persistence.
Features:
- Real-time listeners
- Offline queue management
- Batch operations
- Document/collection mapping
Custom Remote Adapters#
Implement RemoteAdapter<T> to connect to any backend service.
Required Methods:
initialize(): Set up the adaptercreate(T entity): Create entity on remoteread(String id, {String? userId}): Retrieve from remotereadAll({String? userId}): Retrieve all from remotepatch(String id, Map<String, dynamic> delta, {String? userId}): Update on remotedelete(String id, {String? userId}): Delete from remotequery(DatumQuery query, {String? userId}): Execute remote querieschangeStream(): Stream of remote changesgetSyncMetadata(String userId): Get sync metadataunsubscribeFromChanges(): Stop listening to changesresubscribeToChanges(): Resume listening to changes
Adapter Configuration#
DatumConfig#
Configuration options that affect adapter behavior:
DatumConfig<Task>(
// Schema and migrations
schemaVersion: 1,
migrations: [Migration1To2()],
// Sync behavior
autoStartSync: true,
autoSyncInterval: Duration(minutes: 5),
defaultSyncDirection: SyncDirection.pushThenPull,
// Conflict resolution
defaultConflictResolver: LastWriteWinsResolver<Task>(),
// Performance tuning
syncExecutionStrategy: SequentialStrategy(),
syncRequestStrategy: SequentialRequestStrategy(),
remoteEventDebounceTime: Duration(milliseconds: 100),
// Error handling
errorRecoveryStrategy: DatumErrorRecoveryStrategy(
maxRetries: 3,
backoffStrategy: ExponentialBackoffStrategy(),
),
// User switching
defaultUserSwitchStrategy: UserSwitchStrategy.syncThenSwitch,
);
Adapter Registration#
Register adapters during Datum initialization:
await Datum.initialize(
registrations: [
DatumRegistration<Task>(
localAdapter: HiveTaskAdapter(),
remoteAdapter: RestApiTaskAdapter(),
config: DatumConfig<Task>(
// Entity-specific config
),
conflictResolver: CustomConflictResolver(),
middlewares: [EncryptionMiddleware()],
observers: [AuditObserver()],
),
],
);
Adapter Best Practices#
Local Adapter Guidelines#
- Performance: Optimize for fast local operations
- Indexing: Use appropriate indexes for query performance
- Memory Management: Implement efficient caching strategies
- Error Handling: Gracefully handle storage failures
- Migration Support: Implement schema migration logic
Remote Adapter Guidelines#
- Network Efficiency: Minimize requests and payload sizes
- Authentication: Securely handle auth tokens and refresh
- Retry Logic: Implement exponential backoff for failures
- Change Detection: Efficiently detect remote changes
- Rate Limiting: Respect API rate limits and implement throttling
Testing Adapters#
void main() {
group('TaskAdapter', () {
late LocalAdapter<Task> adapter;
setUp(() async {
adapter = HiveTaskAdapter();
await adapter.initialize();
});
tearDown(() async {
await adapter.dispose();
});
test('should create and read task', () async {
final task = Task(id: '1', title: 'Test', userId: 'user1');
await adapter.create(task);
final readTask = await adapter.read('1', userId: 'user1');
expect(readTask?.title, equals('Test'));
});
});
}