Files
tm_app/VIEWMODEL_COMMUNICATION_PATTERNS.md
T

289 lines
6.5 KiB
Markdown

# 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<NotificationStateService>()
.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<bool> checkUnreadNotifications() async {
final result = await api.checkUnread();
_hasUnread = result.hasUnread;
return _hasUnread;
}
Future<void> 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<AppEvent>.broadcast();
Stream<AppEvent> 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<NotificationViewModel>();
final homeVM = context.watch<HomeViewModel>();
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<NotificationState, bool>(
(ref) => NotificationState(),
);
class NotificationState extends StateNotifier<bool> {
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