289 lines
6.5 KiB
Markdown
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
|
|
|