Debug and resolve issues specific to Datum adapters (local and remote).
Local Adapter Issues#
Issue: Hive adapter initialization failure#
Symptoms: Hive.initFlutter() or box opening fails
Common Causes:
- Incorrect path configuration
- Permission issues on device storage
- Concurrent access conflicts
Resolution Steps:
// 1. Check storage permissions
final hasPermission = await Permission.storage.request();
if (!hasPermission) {
throw Exception('Storage permission required for Hive');
}
// 2. Proper initialization order
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive first
final appDir = await getApplicationDocumentsDirectory();
Hive.init(appDir.path);
// Register adapters
Hive.registerAdapter(TaskAdapter());
// Open boxes
final taskBox = await Hive.openBox<Task>('tasks');
// Then initialize Datum
final datum = await Datum.initialize(/* config */);
}
// 3. Handle box opening errors
Future<Box<Task>> openTaskBox() async {
try {
return await Hive.openBox<Task>('tasks');
} catch (e) {
// Try opening with different encryption or recovery
await Hive.deleteBoxFromDisk('tasks'); // Clear corrupted box
return await Hive.openBox<Task>('tasks');
}
}
Issue: Isar database corruption#
Symptoms: Isar queries fail with corruption errors
Recovery Strategies:
class IsarRecoveryManager {
static Future<void> recoverCorruptedDatabase(
String databasePath,
) async {
try {
// Close existing instance
await Isar.getInstance()?.close();
// Delete corrupted files
final dir = Directory(databasePath);
if (await dir.exists()) {
await dir.delete(recursive: true);
}
// Reinitialize
final isar = await Isar.open(
schemas: [TaskSchema],
directory: databasePath,
);
print('Database recovered successfully');
} catch (e) {
print('Recovery failed: $e');
rethrow;
}
}
}
Issue: SQLite database locked#
Symptoms: "Database locked" errors during concurrent operations
Transaction Management:
class SQLiteAdapter extends LocalAdapter<Task> {
final Database _db;
@override
Future<void> saveMany(List<Task> items, String userId) async {
// Use transactions to prevent locking
await _db.transaction((txn) async {
for (final item in items) {
await txn.insert(
'tasks',
item.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
});
}
@override
Future<List<Task>> readAll({String? userId}) async {
// Use read-only transactions for queries
return _db.transaction((txn) async {
final maps = await txn.query('tasks');
return maps.map((map) => Task.fromMap(map)).toList();
});
}
}
Remote Adapter Issues#
Issue: Supabase connection and authentication failures#
Symptoms: Supabase operations fail with connection or auth errors
Common Issues & Solutions:
Connection Setup:
// 1. Verify Supabase configuration
void main() async {
await Supabase.initialize(
url: 'https://your-project.supabase.co',
anonKey: 'your-anon-key',
// Add auth options for better error handling
authOptions: const AuthClientOptions(
autoRefreshToken: true,
persistSession: true,
),
);
}
// 2. Check connection status
class SupabaseHealthCheck {
static Future<bool> isConnected() async {
try {
// Test connection with a simple query
final response = await Supabase.instance.client
.from('health_check')
.select('status')
.limit(1)
.single();
return response != null;
} catch (e) {
print('Supabase connection failed: $e');
return false;
}
}
}
Authentication Issues:
// Handle auth state changes
class SupabaseAuthManager {
StreamSubscription<AuthState>? _authSubscription;
void initializeAuthListener() {
_authSubscription = Supabase.instance.client.auth.onAuthStateChange.listen(
(event) {
switch (event.event) {
case AuthChangeEvent.signedIn:
print('User signed in: ${event.session?.user.id}');
// Initialize Datum sync
break;
case AuthChangeEvent.signedOut:
print('User signed out');
// Pause sync and clear data
Datum.instance.pauseSync();
break;
case AuthChangeEvent.tokenRefreshed:
print('Token refreshed');
// Update adapter with new token
break;
}
},
onError: (error) {
print('Auth error: $error');
// Handle auth errors (network issues, expired tokens, etc.)
},
);
}
void dispose() {
_authSubscription?.cancel();
}
}
RLS (Row Level Security) Issues:
// Debug RLS policies
class SupabaseRLSDebugger {
static Future<void> testRLSPolicies(String userId) async {
try {
// Test read access
final readTest = await Supabase.instance.client
.from('tasks')
.select('*')
.eq('user_id', userId)
.limit(1);
print('Read access: ✅');
// Test write access
final writeTest = await Supabase.instance.client
.from('tasks')
.insert({
'id': const Uuid().v4(),
'user_id': userId,
'title': 'RLS Test',
'created_at': DateTime.now().toIso8601String(),
});
print('Write access: ✅');
} catch (e) {
print('RLS Error: $e');
print('Check your RLS policies in Supabase dashboard');
print('Example policy:');
print('CREATE POLICY "Users can access their own tasks"');
print('ON tasks FOR ALL USING (auth.uid()::text = user_id);');
}
}
}
Real-time Subscription Issues:
class SupabaseRealtimeManager {
RealtimeChannel? _channel;
StreamSubscription? _subscription;
void setupRealtimeSubscription(String userId) {
// Clean up existing subscription
_channel?.unsubscribe();
_subscription?.cancel();
// Create new channel
_channel = Supabase.instance.client.channel('tasks_$userId');
// Subscribe to changes
_channel!.on(
RealtimeListenTypes.postgresChanges,
ChannelFilter(
event: '*',
schema: 'public',
table: 'tasks',
filter: 'user_id=eq.$userId',
),
(payload, [ref]) {
print('Realtime event: ${payload['eventType']}');
handleRealtimeEvent(payload);
},
).subscribe(
(status, [err]) {
if (status == RealtimeSubscribeStatus.subscribed) {
print('Successfully subscribed to realtime updates');
} else {
print('Realtime subscription failed: $err');
// Retry logic
Future.delayed(Duration(seconds: 5), () {
setupRealtimeSubscription(userId);
});
}
},
);
}
void handleRealtimeEvent(Map<String, dynamic> payload) {
final eventType = payload['eventType'];
final newRecord = payload['new'] as Map<String, dynamic>?;
final oldRecord = payload['old'] as Map<String, dynamic>?;
switch (eventType) {
case 'INSERT':
if (newRecord != null) {
final task = Task.fromJson(newRecord);
// Update local cache
}
break;
case 'UPDATE':
if (newRecord != null) {
final task = Task.fromJson(newRecord);
// Update local cache
}
break;
case 'DELETE':
if (oldRecord != null) {
final taskId = oldRecord['id'] as String;
// Remove from local cache
}
break;
}
}
void dispose() {
_channel?.unsubscribe();
_subscription?.cancel();
}
}
Storage and File Upload Issues:
class SupabaseStorageManager {
static Future<String?> uploadFile(
String bucket,
String fileName,
Uint8List fileData,
) async {
try {
final fileExt = fileName.split('.').last;
final filePath = '${DateTime.now().millisecondsSinceEpoch}.$fileExt';
final response = await Supabase.instance.client.storage
.from(bucket)
.uploadBinary(filePath, fileData);
if (response != null) {
// Get public URL
final publicUrl = Supabase.instance.client.storage
.from(bucket)
.getPublicUrl(filePath);
return publicUrl;
}
} catch (e) {
print('File upload failed: $e');
// Check storage permissions
if (e.toString().contains('permission')) {
print('Check storage bucket policies in Supabase dashboard');
}
}
return null;
}
}
Issue: REST API authentication failures#
Symptoms: 401/403 errors from API endpoints
Authentication Handling:
class AuthenticatedRestAdapter extends RemoteAdapter<Task> {
final Dio _dio;
String? _authToken;
AuthenticatedRestAdapter(this._dio) {
_setupInterceptors();
}
void _setupInterceptors() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (_authToken != null) {
options.headers['Authorization'] = 'Bearer $_authToken';
}
return handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// Token expired, try refresh
try {
_authToken = await refreshAuthToken();
// Retry the request
final response = await _dio.request(
error.requestOptions.path,
options: Options(
method: error.requestOptions.method,
headers: {
...error.requestOptions.headers,
'Authorization': 'Bearer $_authToken',
},
),
data: error.requestOptions.data,
);
return handler.resolve(response);
} catch (e) {
// Refresh failed, logout user
await logoutUser();
}
}
return handler.next(error);
},
),
);
}
Future<String?> refreshAuthToken() async {
try {
final response = await _dio.post('/auth/refresh');
return response.data['token'];
} catch (e) {
return null;
}
}
}
Issue: GraphQL adapter query failures#
Symptoms: GraphQL queries return errors or null data
Query Debugging:
class GraphQLAdapter extends RemoteAdapter<Task> {
final GraphQLClient _client;
@override
Future<List<Task>> readAll({String? userId, DatumSyncScope? scope}) async {
const query = '''
query GetTasks($userId: ID!, $limit: Int) {
tasks(userId: $userId, limit: $limit) {
id
title
description
isCompleted
createdAt
modifiedAt
}
}
''';
final options = QueryOptions(
document: gql(query),
variables: {
'userId': userId,
'limit': scope?.limit ?? 100,
},
);
final result = await _client.query(options);
if (result.hasException) {
print('GraphQL Error: ${result.exception}');
// Log detailed error information
for (final error in result.exception!.graphqlErrors) {
print('GraphQL Error: ${error.message}');
print('Path: ${error.path}');
print('Extensions: ${error.extensions}');
}
throw result.exception!;
}
final tasks = result.data?['tasks'] as List? ?? [];
return tasks.map((json) => Task.fromJson(json)).toList();
}
}
Issue: Supabase real-time subscription failures#
Symptoms: Real-time updates not working
Subscription Management:
class SupabaseAdapter extends RemoteAdapter<Task> {
final SupabaseClient _client;
StreamSubscription? _subscription;
void setupRealtimeSync(String userId) {
_subscription?.cancel();
_subscription = _client
.from('tasks')
.stream(primaryKey: ['id'])
.eq('user_id', userId)
.listen((data) {
// Handle real-time updates
for (final change in data) {
switch (change.eventType) {
case PostgresChangeEvent.insert:
_handleInsert(change.newRecord);
break;
case PostgresChangeEvent.update:
_handleUpdate(change.newRecord);
break;
case PostgresChangeEvent.delete:
_handleDelete(change.oldRecord);
break;
}
}
});
}
void _handleInsert(Map<String, dynamic> record) {
final task = Task.fromJson(record);
// Update local cache
localAdapter.create(task);
}
void _handleUpdate(Map<String, dynamic> record) {
final task = Task.fromJson(record);
localAdapter.update(task);
}
void _handleDelete(Map<String, dynamic> record) {
final taskId = record['id'] as String;
localAdapter.delete(taskId);
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
Firebase Adapter Issues#
Issue: Firestore permission errors#
Symptoms: Firestore operations fail with permission-denied
Security Rules Debugging:
// Debug security rules locally
class FirestoreDebugAdapter extends RemoteAdapter<Task> {
final FirebaseFirestore _firestore;
@override
Future<void> create(Task item) async {
try {
await _firestore.collection('tasks').doc(item.id).set(item.toMap());
} catch (e) {
print('Firestore create error: $e');
// Check if it's a permission error
if (e is FirebaseException && e.code == 'permission-denied') {
print('Check Firestore security rules for tasks collection');
print('Current user: ${FirebaseAuth.instance.currentUser?.uid}');
}
rethrow;
}
}
}
// Firestore Security Rules Example
/*
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /tasks/{taskId} {
allow read, write: if request.auth != null &&
request.auth.uid == resource.data.userId;
}
}
}
*/
Issue: Firebase offline persistence conflicts#
Symptoms: Local changes conflict with server state
Offline Persistence Configuration:
class FirebaseAdapter extends RemoteAdapter<Task> {
final FirebaseFirestore _firestore;
FirebaseAdapter() : _firestore = FirebaseFirestore.instance {
// Configure offline persistence
_firestore.settings = const Settings(
persistenceEnabled: true,
cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);
// Enable network calls
_firestore.enableNetwork();
}
@override
Future<List<Task>> readAll({String? userId, DatumSyncScope? scope}) async {
// Force server data for critical reads
final source = scope?.forceServer ?? false
? Source.server
: Source.cache;
final snapshot = await _firestore
.collection('tasks')
.where('userId', isEqualTo: userId)
.get(GetOptions(source: source));
return snapshot.docs
.map((doc) => Task.fromMap(doc.data()))
.toList();
}
}
Adapter Testing#
Unit Testing Adapters#
class AdapterTestSuite {
static Future<void> testLocalAdapter(LocalAdapter<Task> adapter) async {
// Test basic CRUD operations
final testTask = Task.create(title: 'Test Task');
// Create
await adapter.create(testTask);
expect(await adapter.read(testTask.id), equals(testTask));
// Update
final updatedTask = testTask.copyWith(title: 'Updated Task');
await adapter.update(updatedTask);
expect(await adapter.read(testTask.id), equals(updatedTask));
// Delete
await adapter.delete(testTask.id);
expect(await adapter.read(testTask.id), isNull);
}
static Future<void> testRemoteAdapter(RemoteAdapter<Task> adapter) async {
// Mock HTTP responses for testing
final mockTasks = [
Task.create(title: 'Mock Task 1'),
Task.create(title: 'Mock Task 2'),
];
// Test read operations
final tasks = await adapter.readAll(userId: 'test-user');
expect(tasks.length, greaterThan(0));
// Test create operations
final newTask = Task.create(title: 'New Task');
await adapter.create(newTask);
// Verify creation
final readTask = await adapter.read(newTask.id);
expect(readTask?.title, equals(newTask.title));
}
}
Performance Optimization#
Connection Pooling#
class PooledHttpAdapter extends RemoteAdapter<Task> {
final List<Dio> _clients;
int _currentClient = 0;
PooledHttpAdapter(int poolSize) : _clients = List.generate(
poolSize,
(i) => Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 10),
)),
);
Dio get _client {
final client = _clients[_currentClient];
_currentClient = (_currentClient + 1) % _clients.length;
return client;
}
@override
Future<List<Task>> readAll({String? userId, DatumSyncScope? scope}) async {
final response = await _client.get('/tasks', queryParameters: {
'userId': userId,
'limit': scope?.limit,
});
return (response.data as List)
.map((json) => Task.fromJson(json))
.toList();
}
}
Caching Strategies#
class CachedAdapter extends RemoteAdapter<Task> {
final RemoteAdapter<Task> _remoteAdapter;
final Map<String, CachedItem<Task>> _cache = {};
@override
Future<List<Task>> readAll({String? userId, DatumSyncScope? scope}) async {
final cacheKey = 'tasks_$userId';
// Check cache first
final cached = _cache[cacheKey];
if (cached != null && !cached.isExpired) {
return cached.data;
}
// Fetch from remote
final data = await _remoteAdapter.readAll(userId: userId, scope: scope);
// Cache the result
_cache[cacheKey] = CachedItem(data, Duration(minutes: 5));
return data;
}
}
class CachedItem<T> {
final T data;
final DateTime expiry;
CachedItem(this.data, Duration ttl) : expiry = DateTime.now().add(ttl);
bool get isExpired => DateTime.now().isAfter(expiry);
}
Best Practices#
1. Error Handling#
class ResilientAdapter extends RemoteAdapter<Task> {
@override
Future<List<Task>> readAll({String? userId, DatumSyncScope? scope}) async {
const maxRetries = 3;
var attempt = 0;
while (attempt < maxRetries) {
try {
return await _performReadAll(userId, scope);
} catch (e) {
attempt++;
if (attempt >= maxRetries) rethrow;
// Exponential backoff
await Future.delayed(Duration(seconds: attempt * 2));
}
}
throw Exception('Failed after $maxRetries attempts');
}
}
2. Logging and Monitoring#
class MonitoredAdapter extends RemoteAdapter<Task> {
@override
Future<void> create(Task item) async {
final stopwatch = Stopwatch()..start();
try {
await super.create(item);
stopwatch.stop();
await logOperation('create', stopwatch.elapsed, success: true);
} catch (e) {
stopwatch.stop();
await logOperation('create', stopwatch.elapsed, success: false, error: e);
rethrow;
}
}
Future<void> logOperation(
String operation,
Duration duration, {
required bool success,
Object? error,
}) async {
// Send to monitoring service
await monitoringService.logOperation({
'adapter': runtimeType.toString(),
'operation': operation,
'duration_ms': duration.inMilliseconds,
'success': success,
'error': error?.toString(),
'timestamp': DateTime.now().toIso8601String(),
});
}
}
3. Resource Cleanup#
class DisposableAdapter extends RemoteAdapter<Task> implements Disposable {
StreamSubscription? _subscription;
Timer? _healthCheckTimer;
@override
void dispose() {
_subscription?.cancel();
_healthCheckTimer?.cancel();
// Close connections, clean up resources
}
}
For adapter implementation details, check the Adapter Module documentation.