Update .gitignore to exclude Firebase configuration files and refactor notification handling in the app. Removed redundant logging and notification data storage in SharedPreferences. Introduced NotificationCheckService and NotificationStateService for improved notification management. Updated view models and UI components to utilize the new services, enhancing notification state tracking and user experience.
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user