Files
Sage/lib/features/household/services/inventory_sync_service.dart
Dani 3388f24eb4 Add real-time inventory syncing across devices v1.1.0
🔄 Inventory Sync Features:
- Automatic sync to Firebase when adding/updating/deleting items
- Real-time listener pulls changes from other devices
- Bi-directional sync keeps all household devices in sync
- Conflict resolution based on lastModified timestamp
- Firebase version always wins on conflicts

📱 How It Works:
Device A adds item → syncs to Firebase → Device B receives update
Device B updates item → syncs to Firebase → Device A receives update
Device A deletes item → syncs to Firebase → Device B removes item

🔧 Technical Implementation:
- InventorySyncService: Real-time Firestore listener
- Repository hooks: add/update/delete sync to Firebase
- HomeScreen lifecycle: starts/stops sync automatically
- Conflict resolution: newer timestamp wins
- Local Hive + Cloud Firestore hybrid architecture

📁 New Files:
- lib/features/household/services/inventory_sync_service.dart

 Updated Files:
- lib/features/inventory/repositories/inventory_repository_impl.dart
  - Added Firebase sync on add/update/delete operations
  - Maintains local Hive for offline access
- lib/features/home/screens/home_screen.dart
  - Starts sync service on init if in household
  - Stops sync service on dispose

⚠️ Requirements:
- Firebase must be configured (see FIREBASE_SETUP.md)
- Internet connection required for cross-device sync
- Local Hive works offline, syncs when online

 Build Status:
- APK: 63.4MB
- Package: com.github.mystiatech.sage
- Version: 1.1.0+2

🎯 Next Steps for User:
1. Set up Firebase (FIREBASE_SETUP.md)
2. Replace google-services.json with real file
3. Rebuild APK
4. Install on both devices
5. Create/join household
6. Add items → they sync! 🎉

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 15:47:53 -04:00

103 lines
3.3 KiB
Dart

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../data/local/hive_database.dart';
import '../../inventory/models/food_item.dart';
/// Service for syncing inventory items with Firebase in real-time
class InventorySyncService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
StreamSubscription? _itemsSubscription;
/// Start listening to household items from Firebase
Future<void> startSync(String householdId) async {
await stopSync(); // Stop any existing subscription
_itemsSubscription = _firestore
.collection('households')
.doc(householdId)
.collection('items')
.snapshots()
.listen((snapshot) async {
await _handleItemsUpdate(snapshot, householdId);
});
}
/// Stop listening to Firebase updates
Future<void> stopSync() async {
await _itemsSubscription?.cancel();
_itemsSubscription = null;
}
/// Handle updates from Firebase
Future<void> _handleItemsUpdate(
QuerySnapshot snapshot,
String householdId,
) async {
final box = await HiveDatabase.getFoodBox();
// Track Firebase item IDs
final firebaseItemIds = <String>{};
for (final doc in snapshot.docs) {
firebaseItemIds.add(doc.id);
final data = doc.data() as Map<String, dynamic>;
// Check if item exists in local Hive
final itemKey = int.tryParse(doc.id);
if (itemKey != null) {
final existingItem = box.get(itemKey);
// Create or update item
final item = _createFoodItemFromData(data, householdId);
if (existingItem == null) {
// New item from Firebase - add to local Hive with specific key
await box.put(itemKey, item);
} else {
// Update existing item if Firebase version is newer
final firebaseModified = DateTime.parse(data['lastModified'] as String);
final localModified = existingItem.lastModified ?? DateTime(2000);
if (firebaseModified.isAfter(localModified)) {
// Firebase version is newer - update local
await box.put(itemKey, item);
}
}
}
}
// Delete items that no longer exist in Firebase
final itemsToDelete = <int>[];
for (final item in box.values) {
if (item.householdId == householdId && item.key != null) {
if (!firebaseItemIds.contains(item.key.toString())) {
itemsToDelete.add(item.key!);
}
}
}
for (final key in itemsToDelete) {
await box.delete(key);
}
}
/// Create FoodItem from Firebase data
FoodItem _createFoodItemFromData(Map<String, dynamic> data, String householdId) {
return FoodItem()
..name = data['name'] as String
..barcode = data['barcode'] as String?
..quantity = data['quantity'] as int
..unit = data['unit'] as String?
..purchaseDate = DateTime.parse(data['purchaseDate'] as String)
..expirationDate = DateTime.parse(data['expirationDate'] as String)
..locationIndex = data['locationIndex'] as int
..category = data['category'] as String?
..photoUrl = data['photoUrl'] as String?
..notes = data['notes'] as String?
..userId = data['userId'] as String?
..householdId = householdId
..lastModified = DateTime.parse(data['lastModified'] as String)
..syncedToCloud = true;
}
}