# ViewModel Communication Patterns ## ❌ The Problem: Direct ViewModel Dependencies ```dart class NotificationViewModel { final HomeViewModel _homeViewModel; // ❌ BAD: Direct dependency void markAsRead() { _homeViewModel.setUnreadNotificationFalse(); // ❌ Tight coupling } } ``` **Why is this bad?** - Tight coupling between ViewModels - Difficult to test in isolation - Circular dependency risk - Violates Single Responsibility Principle - Hard to maintain and scale --- ## ✅ Solution 1: Shared Service Pattern (BEST for your case) Create a dedicated service that manages shared state. ### Implementation: **Service:** ```dart // lib/data/services/notification_state_service.dart class NotificationStateService extends ChangeNotifier { bool _hasUnreadNotification = false; bool get hasUnreadNotification => _hasUnreadNotification; void setHasUnreadNotification(bool value) { if (_hasUnreadNotification != value) { _hasUnreadNotification = value; notifyListeners(); } } } ``` **ViewModels depend on the service:** ```dart class HomeViewModel { final NotificationStateService _notificationStateService; void checkUnreadNotifications() async { // ... fetch from API _notificationStateService.setHasUnreadNotification(hasUnread); } } class NotificationViewModel { final NotificationStateService _notificationStateService; void markAsRead() { // ... mark as read _notificationStateService.setHasUnreadNotification(false); } } ``` **UI consumes the service directly:** ```dart class NotificationBadge extends StatelessWidget { @override Widget build(BuildContext context) { final hasUnread = context.watch() .hasUnreadNotification; return Badge(showBadge: hasUnread); } } ``` **Benefits:** - ✅ No ViewModel-to-ViewModel dependency - ✅ Single source of truth - ✅ Easy to test - ✅ Follows SOLID principles - ✅ Scalable --- ## ✅ Solution 2: Repository Pattern Move the shared logic to a repository that both ViewModels depend on. ```dart // Repository manages the domain logic class NotificationRepository { bool _hasUnread = false; Future checkUnreadNotifications() async { final result = await api.checkUnread(); _hasUnread = result.hasUnread; return _hasUnread; } Future markAllAsRead() async { await api.markAllAsRead(); _hasUnread = false; } } // ViewModels only depend on repository class HomeViewModel { final NotificationRepository _repo; void init() async { final hasUnread = await _repo.checkUnreadNotifications(); notifyListeners(); } } ``` --- ## ✅ Solution 3: Event Bus / Stream Pattern ViewModels communicate through events without knowing about each other. ```dart // Event bus service class AppEventBus { final _controller = StreamController.broadcast(); Stream get events => _controller.stream; void emit(AppEvent event) => _controller.add(event); } // Events abstract class AppEvent {} class NotificationReadEvent extends AppEvent {} // ViewModel 1: Emits events class NotificationViewModel { final AppEventBus _eventBus; void markAsRead() { // ... mark as read _eventBus.emit(NotificationReadEvent()); } } // ViewModel 2: Listens to events class HomeViewModel { final AppEventBus _eventBus; HomeViewModel() { _eventBus.events.listen((event) { if (event is NotificationReadEvent) { _handleNotificationRead(); } }); } } ``` --- ## ✅ Solution 4: UI Coordination Let the UI layer coordinate between ViewModels. ```dart class NotificationScreen extends StatelessWidget { @override Widget build(BuildContext context) { final notificationVM = context.watch(); final homeVM = context.watch(); return Button( onPressed: () { // UI coordinates the interaction notificationVM.markAsRead(); homeVM.refreshNotificationBadge(); }, ); } } ``` **When to use:** - Simple interactions - UI-specific coordination - Few ViewModels involved --- ## ✅ Solution 5: Riverpod Pattern (Advanced) Use Riverpod's provider dependencies for better state management. ```dart // Shared state provider final notificationStateProvider = StateNotifierProvider( (ref) => NotificationState(), ); class NotificationState extends StateNotifier { NotificationState() : super(false); void setHasUnread(bool value) => state = value; } // ViewModels depend on the provider final homeViewModelProvider = Provider((ref) { final notificationState = ref.read(notificationStateProvider.notifier); return HomeViewModel(notificationState); }); ``` --- ## 📊 Comparison Table | Pattern | Complexity | Testability | Scalability | Best For | |---------|-----------|-------------|-------------|----------| | Shared Service | Low | High | High | Shared state across app | | Repository | Medium | High | High | Domain logic sharing | | Event Bus | Medium | Medium | High | Loosely coupled events | | UI Coordination | Low | Medium | Low | Simple interactions | | Riverpod | Medium | High | Very High | New projects | --- ## 🎯 Recommendation for Your Project Use **Solution 1: Shared Service Pattern** because: 1. ✅ Your codebase already uses Provider 2. ✅ The notification state is truly shared across the app 3. ✅ Simple to implement and understand 4. ✅ Easy to test 5. ✅ Follows your modular architecture principle 6. ✅ No major refactoring required --- ## 📝 Implementation Checklist - [x] Create `NotificationStateService` - [x] Add service to dependency injection - [x] Update `HomeViewModel` to use service - [x] Update `NotificationViewModel` to use service - [x] Remove direct ViewModel dependencies - [x] Test the implementation - [ ] Remove the example files when done --- ## 🧪 Testing Example ```dart test('NotificationViewModel updates shared state', () { final stateService = NotificationStateService(); final repository = MockNotificationRepository(); final viewModel = NotificationViewModel( notificationsRepository: repository, notificationStateService: stateService, ); // Act viewModel.markAllAsRead(); // Assert expect(stateService.hasUnreadNotification, false); }); ``` --- ## 🚀 Key Takeaway **ViewModels should NEVER depend on other ViewModels.** Instead: - Use a **shared service** for shared state - Use **repositories** for domain logic - Use **event buses** for loose coupling - Let the **UI coordinate** simple interactions